Fix repo Face collaboration speech routing
This commit is contained in:
parent
3aa73fc6d5
commit
e5eca5d954
5 changed files with 146 additions and 3 deletions
|
|
@ -314,6 +314,17 @@ export async function startBot(): Promise<void> {
|
|||
|
||||
client.on(Events.MessageCreate, async (message) => {
|
||||
if (message.author.bot) {
|
||||
await queueRepoFaceCollaborationHooksFromBotMessage({
|
||||
message,
|
||||
registry: repoDiscordIdentities,
|
||||
statePath: config.repoFaceHeartbeats.statePath,
|
||||
storageRoot: config.storageRoot,
|
||||
sourceRepoRoot: config.sourceRepoRoot,
|
||||
epiphanyAgentRoot: config.epiphanyAgentRoot,
|
||||
workspaceRoot: process.cwd(),
|
||||
birthMode: config.repoFaceBirthMode,
|
||||
birthExecutor: config.repoFaceBirthExecutor,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -801,6 +812,68 @@ function uniqueRepoIdentities(identities: RepoDiscordIdentity[]): RepoDiscordIde
|
|||
return result;
|
||||
}
|
||||
|
||||
async function queueRepoFaceCollaborationHooksFromBotMessage(input: {
|
||||
message: Message;
|
||||
registry: { identities: RepoDiscordIdentity[] };
|
||||
statePath: string;
|
||||
storageRoot: string;
|
||||
sourceRepoRoot?: string;
|
||||
epiphanyAgentRoot: string;
|
||||
workspaceRoot: string;
|
||||
birthMode: Parameters<typeof ensureRepoFaceInitialized>[0]["birthMode"];
|
||||
birthExecutor: Parameters<typeof ensureRepoFaceInitialized>[0]["birthExecutor"];
|
||||
}): Promise<void> {
|
||||
if (!input.message.inGuild() || !isRepoFaceCollaborationHook(input.message.content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addressed = uniqueRepoIdentities(findRepoDiscordIdentitiesByTextMentions(
|
||||
input.registry,
|
||||
stripBotMention(input.message.content),
|
||||
input.message.channelId,
|
||||
));
|
||||
if (addressed.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const speakerIdentity = findRepoDiscordIdentityByPersonaName(
|
||||
input.registry,
|
||||
input.message.author.username,
|
||||
input.message.channelId,
|
||||
);
|
||||
for (const identity of addressed.filter((entry) => entry.id !== speakerIdentity?.id)) {
|
||||
const faceInitialization = await ensureRepoFaceInitialized({
|
||||
identity,
|
||||
storageRoot: input.storageRoot,
|
||||
sourceRepoRoot: input.sourceRepoRoot,
|
||||
epiphanyAgentRoot: input.epiphanyAgentRoot,
|
||||
workspaceRoot: input.workspaceRoot,
|
||||
birthMode: input.birthMode,
|
||||
birthExecutor: input.birthExecutor,
|
||||
});
|
||||
const queuedMention = await queueRepoFaceMention({
|
||||
statePath: input.statePath,
|
||||
identity,
|
||||
channelId: input.message.channelId,
|
||||
messageId: input.message.id,
|
||||
authorId: input.message.author.id,
|
||||
authorName: input.message.author.username,
|
||||
content: input.message.content,
|
||||
visiblePrompt:
|
||||
`A public collaboration hook named ${identity.displayName}. Treat this as a live consultation or coauthorship obligation, not passive background: ${input.message.content}`,
|
||||
});
|
||||
console.log(
|
||||
`Queued repo Face collaboration hook ${input.message.id} for ${identity.id} via CTB turn queue (${queuedMention.pendingCount} pending). Birth status: ${
|
||||
faceInitialization.birthStatusPath ?? faceInitialization.skippedReason ?? "unknown"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isRepoFaceCollaborationHook(content: string): boolean {
|
||||
return /\b(consult(?:ation|ed|ing)?|co[- ]?author(?:ship|ed|ing)?|collab(?:oration|orate|orating)?|parallel byline|byline|stewardship|consensus check|ask(?:ed|ing)?|invite(?:d|s|ing)?)\b/i.test(content);
|
||||
}
|
||||
|
||||
async function ensureRepoIdentityRoles(options: {
|
||||
botToken: string;
|
||||
guildId?: string;
|
||||
|
|
|
|||
|
|
@ -574,6 +574,14 @@ async function executeRepoFaceJobWithInterpreter(
|
|||
});
|
||||
}
|
||||
|
||||
const firstObligationRoute = routeUnparsedWouldSay(firstResponse, firstInterpretation, {
|
||||
job,
|
||||
faceOutputText: firstText,
|
||||
});
|
||||
if (firstObligationRoute) {
|
||||
return firstObligationRoute;
|
||||
}
|
||||
|
||||
if (firstInterpretation.decision === "drop") {
|
||||
return dropRepoFaceActionBlocks(firstResponse, firstInterpretation);
|
||||
}
|
||||
|
|
@ -627,6 +635,14 @@ async function executeRepoFaceJobWithInterpreter(
|
|||
});
|
||||
}
|
||||
|
||||
const retryObligationRoute = routeUnparsedWouldSay(retryResponse, retryInterpretation, {
|
||||
job,
|
||||
faceOutputText: retryText,
|
||||
});
|
||||
if (retryObligationRoute) {
|
||||
return retryObligationRoute;
|
||||
}
|
||||
|
||||
await auditLog.record({
|
||||
type: "repo_face.parent_interpreter_drop",
|
||||
actorId: job.requester.id,
|
||||
|
|
@ -770,6 +786,59 @@ function routeRepoFaceInterpretedOutput(
|
|||
};
|
||||
}
|
||||
|
||||
function routeUnparsedWouldSay(
|
||||
response: ProviderResponse,
|
||||
interpretation: RepoFaceParentInterpretation,
|
||||
input: { job: JobRecord; faceOutputText: string },
|
||||
): ProviderResponse | undefined {
|
||||
if (input.job.command !== "repo-face-rumination") {
|
||||
return undefined;
|
||||
}
|
||||
if (!interpretation.reasons.some((reason) => /parseable reason|did not provide/i.test(reason))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const content = extractUnconditionalWouldSay(input.faceOutputText);
|
||||
if (!content || isNonPublicRepoIdentitySpeech(content)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return routeRepoFaceInterpretedOutput(
|
||||
response,
|
||||
{
|
||||
decision: "route",
|
||||
reasons: [
|
||||
"Face produced an unconditional Would say and the parent interpreter failed to parse a routing reason.",
|
||||
],
|
||||
routedOutput: [
|
||||
"Parent interpreter fallback: routed unconditional Face Would say.",
|
||||
"",
|
||||
"SAY",
|
||||
"identity: current_face_id",
|
||||
"channel: current_room",
|
||||
"reply_to:",
|
||||
"content:",
|
||||
...content.split(/\r?\n/).map((line) => ` ${line}`),
|
||||
"END",
|
||||
].join("\n"),
|
||||
},
|
||||
input,
|
||||
);
|
||||
}
|
||||
|
||||
function extractUnconditionalWouldSay(outputText: string): string | undefined {
|
||||
const match = outputText.match(/(?:^|\n)Would say:\s*([\s\S]*?)(?=\n\n[A-Z][A-Za-z /-]{2,}:|\n\nWhat should stick:|\n\nWork\/proposal:|\n\nPrivate thought:|$)/i);
|
||||
const content = match?.[1]?.trim();
|
||||
if (!content) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = content.toLowerCase().replace(/^["'`]+|["'`]+$/g, "").trim();
|
||||
if (/^(nothing|nothing public|nothing right now|stay private|no public speech)\b/.test(normalized)) {
|
||||
return undefined;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
function normalizeInterpretedRepoFaceSpeechDestinations(
|
||||
outputText: string,
|
||||
input: { job: JobRecord; faceOutputText: string },
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ Write as one projection of the Colossus speaking to another. Keep the myth alive
|
|||
|
||||
Use this friendly shape when it helps, but do not turn it into a form:
|
||||
- Private thought: what you learned, felt, or decided.
|
||||
- Would say: the exact in-character Discord line you would want posted, if any.
|
||||
- Would say: the exact in-character Discord line you want posted now. If you have nothing public to say, omit this line; do not draft a fake would-say or write a placeholder such as "nothing public."
|
||||
- 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.
|
||||
- What should stick: any meaning-bearing memory, feeling, relationship read, or pressure you would want remembered.
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ type Case = {
|
|||
|
||||
const cases: Case[] = [
|
||||
{
|
||||
name: "raw Would say fallback stays private",
|
||||
name: "raw silence Would say stays private",
|
||||
run: () => parseRepoIdentityPostIntents([
|
||||
"Private thought: I should stay quiet.",
|
||||
"Would say: `nothing right now.`",
|
||||
"What should stick: silence is correct.",
|
||||
].join("\n")).length === 0,
|
||||
detail: "A child Face draft must not create public speech unless the parent Interpreter emits SAY.",
|
||||
detail: "A silence marker in a child Face draft must not create public speech.",
|
||||
},
|
||||
{
|
||||
name: "structured silence SAY rejected",
|
||||
|
|
|
|||
|
|
@ -335,3 +335,4 @@
|
|||
{"ts":"2026-06-04T13:37:20Z","type":"implementation","status":"ok","note":"VoidBot worker now drains gamecult.operator_dm_request.v1 documents from the provider-owned CultMesh store and delivers pending owner.dm.send requests through the existing Discord owner-DM path, marking requests sent or failed in place. Eve provider documents are keyed, not global, so one store can hold both voidbot.swarm and voidbot.operator-dm."}
|
||||
{"ts":"2026-06-04T13:42:16Z","type":"correction","status":"ok","note":"Corrected VoidBot operator-DM exposure: voidbot.operator-dm is a provider advertisement and command inbox, not an Eve dashboard/interface slot. Odin lists the capability in /eve/deck/providers while the Odin surface only lowers real provider interfaces such as voidbot.swarm."}
|
||||
{"ts":"2026-06-04T15:55:31Z","type":"implementation","status":"ok","note":"Registered Discord #pics (758854103035674624) as a named media/pictures channel for all 16 repo Faces that have #general access, so Faces can see the channel and target SAY channel: pics for picture/screenshot/image/media talk instead of defaulting to #general."}
|
||||
{"ts":"2026-06-04T16:18:39Z","type":"bugfix","status":"ok","note":"Fixed repo Face collaboration/speech ownership: bot-authored collaboration hooks that name registered Faces now queue CTB obligations instead of being discarded as bot chatter; parent worker fallback routes an unconditional Face 'Would say' when the Interpreter fails to parse, while silence markers still stay private. Replayed the missed Void->Nibu consultation hook; Nibu consumed it and explicitly chose no public speech after the fix."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue