Wake Faces on broadcast invitations
This commit is contained in:
parent
c430e709fd
commit
ae6eabcd4d
3 changed files with 84 additions and 17 deletions
|
|
@ -21,6 +21,7 @@ import {
|
|||
findRepoDiscordIdentityByRoleIds,
|
||||
findRepoDiscordIdentitiesByTextMentions,
|
||||
findRepoDiscordIdentityByTextAddress,
|
||||
isRepoDiscordIdentityAllowedInChannel,
|
||||
loadFaceIdentityRegistry,
|
||||
queueAgentHeartbeatMention,
|
||||
queueRepoFaceMention,
|
||||
|
|
@ -364,10 +365,19 @@ export async function startBot(): Promise<void> {
|
|||
...textMentionedRepoIdentities,
|
||||
...(repliedRepoIdentity ? [repliedRepoIdentity] : []),
|
||||
]);
|
||||
const broadcastAddressedRepoIdentities = !isDirectMessage && addressedRepoIdentities.length === 0 &&
|
||||
isRepoFaceBroadcastInvitation(stripBotMention(message.content))
|
||||
? repoDiscordIdentities.identities.filter((identity) =>
|
||||
isRepoDiscordIdentityAllowedInChannel(identity, message.channelId),
|
||||
)
|
||||
: [];
|
||||
const pendingRepoIdentities = addressedRepoIdentities.length > 0
|
||||
? addressedRepoIdentities
|
||||
: broadcastAddressedRepoIdentities;
|
||||
const addressedRepoIdentity = roleAddressedRepoIdentity ?? textAddressedRepoIdentity ?? repliedRepoIdentity;
|
||||
const isBotMentioned = Boolean(client.user && message.mentions.has(client.user));
|
||||
|
||||
if (!client.user || (!isDirectMessage && !isBotMentioned && addressedRepoIdentities.length === 0)) {
|
||||
if (!client.user || (!isDirectMessage && !isBotMentioned && pendingRepoIdentities.length === 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -385,15 +395,16 @@ export async function startBot(): Promise<void> {
|
|||
: stripBotMention(message.content).trim();
|
||||
|
||||
try {
|
||||
if (addressedRepoIdentities.length > 0) {
|
||||
if (pendingRepoIdentities.length > 0) {
|
||||
let queuedCount = 0;
|
||||
for (const identity of addressedRepoIdentities) {
|
||||
for (const identity of pendingRepoIdentities) {
|
||||
const identityVisiblePrompt = renderRepoIdentityVisiblePrompt({
|
||||
message,
|
||||
identity,
|
||||
roleAddressed: roleAddressedRepoIdentities.some((entry) => entry.id === identity.id),
|
||||
textAddressed: textAddressedRepoIdentity?.id === identity.id,
|
||||
replied: repliedRepoIdentity?.id === identity.id,
|
||||
broadcastAddressed: broadcastAddressedRepoIdentities.some((entry) => entry.id === identity.id),
|
||||
});
|
||||
if (!identityVisiblePrompt) {
|
||||
console.log(`Ignored empty repo Face mention ${message.id} for ${identity.id}.`);
|
||||
|
|
@ -730,6 +741,7 @@ function renderRepoIdentityVisiblePrompt(input: {
|
|||
roleAddressed: boolean;
|
||||
textAddressed: boolean;
|
||||
replied: boolean;
|
||||
broadcastAddressed: boolean;
|
||||
}): string {
|
||||
const strippedBotMention = stripBotMention(input.message.content);
|
||||
if (input.roleAddressed || input.textAddressed) {
|
||||
|
|
@ -742,6 +754,10 @@ function renderRepoIdentityVisiblePrompt(input: {
|
|||
}
|
||||
}
|
||||
|
||||
if (input.broadcastAddressed && strippedBotMention.trim().length > 0) {
|
||||
return `The room was explicitly invited to answer. For ${input.identity.displayName}, this invitation is live: ${strippedBotMention.trim()}`;
|
||||
}
|
||||
|
||||
if (input.message.content.trim().length > 0) {
|
||||
return strippedBotMention.trim();
|
||||
}
|
||||
|
|
@ -753,6 +769,24 @@ function renderRepoIdentityVisiblePrompt(input: {
|
|||
);
|
||||
}
|
||||
|
||||
function isRepoFaceBroadcastInvitation(content: string): boolean {
|
||||
const normalized = collapseWhitespace(content).toLowerCase();
|
||||
if (normalized.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
/\b(everyone|everybody|all of you|anyone|anybody|the swarm|you all|y'all)\b/,
|
||||
/\bhow (are|is) (everyone|everybody|you all|y'all)\b/,
|
||||
/\bwhat about (your|everyone's|everybody's) (minds?|bodies?|feelings?)\b/,
|
||||
/\banything i can do to help\b/,
|
||||
].some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
|
||||
function collapseWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function uniqueRepoIdentities(identities: RepoDiscordIdentity[]): RepoDiscordIdentity[] {
|
||||
const seen = new Set<string>();
|
||||
const result: RepoDiscordIdentity[] = [];
|
||||
|
|
|
|||
|
|
@ -1185,7 +1185,7 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
identity.displayName,
|
||||
...(identity.avatarUrl ? ["--persona-avatar-url", identity.avatarUrl] : []),
|
||||
...(intent.replyToMessageId ? ["--reply-to-message-id", intent.replyToMessageId] : []),
|
||||
]);
|
||||
], { retries: 1 });
|
||||
if (!posted.messageId || !posted.transport) {
|
||||
throw new Error(`Bifrost Discord bridge returned no message receipt for job ${job.id}.`);
|
||||
}
|
||||
|
|
@ -1871,23 +1871,55 @@ function bifrostMirrorArgs(
|
|||
];
|
||||
}
|
||||
|
||||
function runBifrostBridge(args: string[]): BifrostBridgeReceipt {
|
||||
function runBifrostBridge(args: string[], options: { retries?: number } = {}): BifrostBridgeReceipt {
|
||||
const bridgeScript = resolve(config.bifrostRoot, "tools", "bifrost-bridge.mjs");
|
||||
const result = spawnSync("node", [bridgeScript, ...args], {
|
||||
cwd: config.bifrostRoot,
|
||||
encoding: "utf8",
|
||||
windowsHide: true,
|
||||
});
|
||||
const maxAttempts = 1 + Math.max(0, Math.floor(options.retries ?? 0));
|
||||
let lastFailure = "";
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Bifrost bridge failed: ${result.stderr || result.stdout}`);
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
const result = spawnSync("node", [bridgeScript, ...args], {
|
||||
cwd: config.bifrostRoot,
|
||||
encoding: "utf8",
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
if (result.status !== 0 || result.error) {
|
||||
lastFailure = renderBridgeFailure(result, attempt, maxAttempts);
|
||||
if (attempt < maxAttempts) {
|
||||
console.warn(`Bifrost bridge attempt ${attempt}/${maxAttempts} failed; retrying. ${lastFailure}`);
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Bifrost bridge failed. ${lastFailure}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(result.stdout) as BifrostBridgeReceipt;
|
||||
} catch {
|
||||
throw new Error(`Bifrost bridge returned non-JSON output: ${result.stdout}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(result.stdout) as BifrostBridgeReceipt;
|
||||
} catch {
|
||||
throw new Error(`Bifrost bridge returned non-JSON output: ${result.stdout}`);
|
||||
}
|
||||
throw new Error(`Bifrost bridge failed. ${lastFailure || "No bridge attempt completed."}`);
|
||||
}
|
||||
|
||||
function renderBridgeFailure(
|
||||
result: ReturnType<typeof spawnSync>,
|
||||
attempt: number,
|
||||
maxAttempts: number,
|
||||
): string {
|
||||
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
||||
const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
||||
const errorMessage = result.error instanceof Error ? result.error.message : undefined;
|
||||
return [
|
||||
`attempt=${attempt}/${maxAttempts}`,
|
||||
`status=${result.status ?? "null"}`,
|
||||
`signal=${result.signal ?? "none"}`,
|
||||
errorMessage ? `error=${errorMessage}` : undefined,
|
||||
stderr ? `stderr=${stderr}` : undefined,
|
||||
stdout ? `stdout=${stdout}` : undefined,
|
||||
]
|
||||
.filter((entry): entry is string => Boolean(entry))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function parseRepoIdentityUpdateRequestIntents(finalResponse: string): RepoIdentityUpdateRequestIntent[] {
|
||||
|
|
|
|||
|
|
@ -311,3 +311,4 @@
|
|||
{"ts":"2026-05-24T02:14:42Z","type":"ops","status":"ok","note":"Thawed state/agent-swarm-paused.json and manually pulsed GameCult Local Orchestrator for repo-face-heartbeats. The pulse ran successfully, queued Void moderation, resynced CTB wall-clock initiative, and left repo Faces active under idle cooling with Bifrost dispatch still disabled."}
|
||||
{"ts":"2026-05-24T02:24:32Z","type":"implementation","status":"ok","note":"Repo Face turns now carry Discord attachment metadata through SourceMessage, cache recent image attachments under .voidbot/media/discord-images, render media markers in Face transcripts, and pass cached image files to owner Codex via codex exec --image. Prompt assembly verified the Image awareness section renders and non-image attachments stay metadata-only."}
|
||||
{"ts":"2026-05-24T02:27:41Z","type":"ops","status":"ok","note":"Rebuilt VoidBot and restarted the live stack after multimodal Face-turn changes. Runtime status is ready with bot PID 11324 and worker PID 16396, so owner-Codex provider changes for codex exec --image are live in dist."}
|
||||
{"ts":"2026-05-24T02:54:54Z","type":"implementation","status":"ok","note":"Discord repo Face intake now treats explicit room-wide invitations as pending CTB obligations for every eligible Face in the channel, and repo identity Discord bridge posting retries once while reporting status/signal/stdout/stderr instead of failing with an empty message."}
|
||||
|
|
|
|||
Loading…
Reference in a new issue