Attach repo Face speech to conversation contexts
This commit is contained in:
parent
9d6b51fc02
commit
b76f6c54a7
9 changed files with 241 additions and 16 deletions
|
|
@ -938,12 +938,15 @@ function normalizeInterpretedRepoFaceSpeechDestinations(
|
|||
if (input.job.command !== "repo-face-rumination") {
|
||||
return outputText;
|
||||
}
|
||||
if (repoFaceOutputHasExplicitSayChannel(input.faceOutputText)) {
|
||||
if (repoFaceOutputHasExplicitSayDestination(outputText) || repoFaceOutputHasExplicitSayDestination(input.faceOutputText)) {
|
||||
return outputText;
|
||||
}
|
||||
if (!input.job.guildContext?.channelId) {
|
||||
return outputText;
|
||||
}
|
||||
if (repoFaceHasMultipleActiveConversationChannels(input.job)) {
|
||||
return outputText;
|
||||
}
|
||||
|
||||
return rewriteRepoFaceSayChannels(
|
||||
outputText,
|
||||
|
|
@ -951,10 +954,22 @@ function normalizeInterpretedRepoFaceSpeechDestinations(
|
|||
);
|
||||
}
|
||||
|
||||
function repoFaceOutputHasExplicitSayChannel(outputText: string): boolean {
|
||||
function repoFaceHasMultipleActiveConversationChannels(job: JobRecord): boolean {
|
||||
return repoFaceHasMultipleActiveConversationChannels(job);
|
||||
}
|
||||
|
||||
function repoFaceOutputHasExplicitSayDestination(outputText: string): boolean {
|
||||
return parseRepoFaceActionBlocks(outputText).some((block) =>
|
||||
block.kind === "say" &&
|
||||
Boolean(optionalDslString(block.fields.channel) ?? optionalDslString(block.fields.channelId))
|
||||
Boolean(
|
||||
optionalDslString(block.fields.channel) ??
|
||||
optionalDslString(block.fields.channelId) ??
|
||||
optionalDslString(block.fields.reply_to) ??
|
||||
optionalDslString(block.fields.replyToMessageId) ??
|
||||
optionalDslString(block.fields.context) ??
|
||||
optionalDslString(block.fields.context_id) ??
|
||||
optionalDslString(block.fields.contextId),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1362,13 +1377,32 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
return false;
|
||||
}
|
||||
|
||||
const replyTargetChannelId = resolveRepoIdentityReplyTargetChannel(job, intent.replyToMessageId);
|
||||
const conversationContext = resolveRepoIdentityConversationContext(job, intent.contextId);
|
||||
if (intent.contextId && !conversationContext) {
|
||||
console.warn(
|
||||
`Rejected repo identity ${identity.id} speech for job ${job.id}: unknown conversation context "${intent.contextId}".`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (repoIdentityRequiresExplicitConversationContext(job, intent) && !conversationContext) {
|
||||
console.warn(
|
||||
`Rejected repo identity ${identity.id} speech for job ${job.id}: multiple active conversation contexts require context, reply_to, or channel.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const effectiveReplyToMessageId = intent.replyToMessageId ?? conversationContext?.messageId;
|
||||
const replyTargetChannelId = resolveRepoIdentityReplyTargetChannel(job, effectiveReplyToMessageId);
|
||||
if (replyTargetChannelId && intent.channelId && normalizeChannelSelector(intent.channelId) !== normalizeChannelSelector(replyTargetChannelId)) {
|
||||
console.warn(
|
||||
`Coerced repo identity ${identity.id} speech for job ${job.id} from requested channel "${intent.channelId}" to reply target channel ${replyTargetChannelId}.`,
|
||||
);
|
||||
}
|
||||
const requestedChannelId = replyTargetChannelId ?? intent.channelId ?? repoIdentityDefaultSpeechChannel(job);
|
||||
const requestedChannelId = replyTargetChannelId
|
||||
?? conversationContext?.channelId
|
||||
?? intent.channelId
|
||||
?? repoIdentityConversationFocusChannel(job)
|
||||
?? 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.`);
|
||||
|
|
@ -1442,7 +1476,7 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
"--persona-name",
|
||||
identity.displayName,
|
||||
...(identity.avatarUrl ? ["--persona-avatar-url", identity.avatarUrl] : []),
|
||||
...(intent.replyToMessageId ? ["--reply-to-message-id", intent.replyToMessageId] : []),
|
||||
...(effectiveReplyToMessageId ? ["--reply-to-message-id", effectiveReplyToMessageId] : []),
|
||||
], { retries: 1 });
|
||||
if (!posted.messageId || !posted.transport) {
|
||||
throw new Error(`Bifrost Discord bridge returned no message receipt for job ${job.id}.`);
|
||||
|
|
@ -1451,7 +1485,7 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
identity,
|
||||
channelId,
|
||||
content,
|
||||
replyToMessageId: intent.replyToMessageId,
|
||||
replyToMessageId: effectiveReplyToMessageId,
|
||||
messageId: posted.messageId,
|
||||
transport: posted.transport,
|
||||
}).catch((error: unknown) => {
|
||||
|
|
@ -1463,7 +1497,7 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
|
|||
identity,
|
||||
channelId,
|
||||
content,
|
||||
replyToMessageId: intent.replyToMessageId,
|
||||
replyToMessageId: effectiveReplyToMessageId,
|
||||
messageId: posted.messageId,
|
||||
});
|
||||
console.log(
|
||||
|
|
@ -3034,6 +3068,45 @@ function repoIdentityDefaultSpeechChannel(job: JobRecord): string | undefined {
|
|||
: job.outputChannelId;
|
||||
}
|
||||
|
||||
function repoIdentityConversationFocusChannel(job: JobRecord): string | undefined {
|
||||
const focus = job.contextBundle.repoFaceConversationFocus;
|
||||
if (!focus || focus.isCurrentRoom) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nonCurrentThreads = (job.contextBundle.repoFaceConversationThreads ?? [])
|
||||
.filter((thread) => !thread.isCurrentRoom);
|
||||
return nonCurrentThreads.length <= 1 || focus.reason === "pending_mention"
|
||||
? focus.channelId
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function resolveRepoIdentityConversationContext(
|
||||
job: JobRecord,
|
||||
contextId: string | undefined,
|
||||
): NonNullable<JobRecord["contextBundle"]["repoFaceConversationThreads"]>[number] | undefined {
|
||||
const normalized = contextId?.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (job.contextBundle.repoFaceConversationThreads ?? [])
|
||||
.find((thread) => thread.contextId === normalized);
|
||||
}
|
||||
|
||||
function repoIdentityRequiresExplicitConversationContext(
|
||||
job: JobRecord,
|
||||
intent: RepoIdentityPostIntent,
|
||||
): boolean {
|
||||
if (intent.channelId || intent.replyToMessageId || intent.contextId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const activeThreads = job.contextBundle.repoFaceConversationThreads ?? [];
|
||||
const representedChannels = new Set(activeThreads.map((thread) => thread.channelId));
|
||||
return representedChannels.size > 1;
|
||||
}
|
||||
|
||||
function resolveRepoIdentityReplyTargetChannel(
|
||||
job: JobRecord,
|
||||
replyToMessageId: string | undefined,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ export const REPO_IDENTITY_POST_SENTINEL = "VOIDBOT_REPO_IDENTITY_POST:";
|
|||
|
||||
export interface RepoIdentityPostIntent {
|
||||
identity?: string;
|
||||
contextId?: string;
|
||||
channelId?: string;
|
||||
content: string;
|
||||
replyToMessageId?: string;
|
||||
|
|
@ -29,6 +30,7 @@ export function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentit
|
|||
return [
|
||||
{
|
||||
identity: optionalDslString(block.fields.identity),
|
||||
contextId: optionalDslString(block.fields.context) ?? optionalDslString(block.fields.context_id) ?? optionalDslString(block.fields.contextId),
|
||||
channelId: optionalDslString(block.fields.channel) ?? optionalDslString(block.fields.channelId),
|
||||
replyToMessageId: optionalDslString(block.fields.reply_to) ?? optionalDslString(block.fields.replyToMessageId),
|
||||
content,
|
||||
|
|
@ -51,6 +53,7 @@ export function parseRepoIdentityPostIntents(finalResponse: string): RepoIdentit
|
|||
}
|
||||
intents.push({
|
||||
identity: typeof parsed.identity === "string" ? parsed.identity.trim() : undefined,
|
||||
contextId: typeof parsed.contextId === "string" ? parsed.contextId.trim() : undefined,
|
||||
channelId: typeof parsed.channelId === "string" ? parsed.channelId.trim() : undefined,
|
||||
replyToMessageId:
|
||||
typeof parsed.replyToMessageId === "string" ? parsed.replyToMessageId.trim() : undefined,
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- Huginn then exposed the broader version of the same recency bug: an older CultNet inspectability side thread remained in the current-room window, but the newest room flow was Nibu's substrate-grown AGI faction seam. Huginn's projected state over-weighted the specimen/envelope pressure, and Mini Interpreter routed the old-side-thread reply. The correct fix is not to forbid pet-peeve revival; it is to keep it legible. Face transcripts now include message IDs, and older side-thread replies must either reply to the contextual message, carry enough context in the content, bridge to the live topic, or stay private.
|
||||
- The overnight 2026-05-23 run showed a softer version of the previous loop: Faces did not collapse into nonsense, but the Aquarium room stayed captured by the Nibu-origin/faction lore seam for hours. Existing `saturationScore` lived on private thought/incubation surfaces, and the repetition prompt mostly caught copied openings/endings, not semantic topic attractors. The heartbeat prompt now projects current-room topic saturation from repeated raw-transcript terms and estimates whether the attractor is home-adjacent or another steward's gravity well from each Face's identity/jurisdiction text. The state projector should turn foreign attractors into character-specific crowding, neglect, boredom, rivalry, handoff, or pivot pressure; the Interpreter diagnoses non-owner same-topic politeness as missing personal stake, ownership, freshness, handoff, or pivot rather than applying a topic ban or token blacklist.
|
||||
- Void's broader moderation authority had become venue leakage: recent-history projection stripped `channelId`, leaving the configured `publicSpeechTarget` as the obvious place for replies, so Aquarium-shaped talk could land in `#general`. The rumination context now preserves message `channelId` and projects `recentConversationTarget`; prompt doctrine says source-channel replies belong in that source channel, while `publicSpeechTarget` is for unanchored herald/artifact announcements.
|
||||
- Repo Face speech routing now uses active conversation contexts instead of one room-focus guess. Heartbeat prompts project up to six `ctx_<channel>_<message>` records across current and nearby channels; the Interpreter can emit `context:` on a `SAY`; the worker resolves that context to the source channel/message and rejects unknown contexts or ambiguous bare SAY blocks when multiple active channels are visible. A Persona can carry separate #aquarium/#pics/#general conversations at once, but public speech has to keep its context attached.
|
||||
- The Nibu/Void turf-crossing discussion exposed a social-state projection gap: existing bonds/status reads project well once typed state already contains them, but the live pipeline did not own "this recent event should become relationship/status pressure." The repair adds bounded raw relationship-pressure evidence to the state packet before the projector: recent messages that touch the Face, peer Faces, existing social targets, jurisdiction/domain terms, or status/territory/consultation language. The projector may turn that evidence into tentative felt pressure, and the Interpreter may persist a bond/status/mood note only when the projected memory or Face output shows a character-specific reaction. This should let resentment, rivalry, consultation offense, and territorial tension emerge without manual state edits.
|
||||
- The swarm brake currently reads `state/agent-swarm-paused.json` with `"paused": false`. If scheduled Face turns start overrunning again, use that brake rather than letting the orchestrator churn. The known 2026-06-09 timeout source was the model state-projector child; local runtime currently keeps it disabled while the deterministic fallback and Interpreter/worker speech rails are watched.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
type GuildContext,
|
||||
type InteractionMemoryProfile,
|
||||
type PromptImageAttachment,
|
||||
type RepoFaceConversationFocus,
|
||||
type RetrievalResult,
|
||||
type SituationalSocialRead,
|
||||
type SourceGroundingHint,
|
||||
|
|
@ -18,6 +19,8 @@ export interface BuildContextInput {
|
|||
guildContext: GuildContext;
|
||||
recentMessages?: SourceMessage[];
|
||||
imageAttachments?: PromptImageAttachment[];
|
||||
repoFaceConversationFocus?: RepoFaceConversationFocus;
|
||||
repoFaceConversationThreads?: RepoFaceConversationFocus[];
|
||||
retrieval?: RetrievalResult[];
|
||||
interactionMemory?: InteractionMemoryProfile;
|
||||
situationalSocialRead?: SituationalSocialRead;
|
||||
|
|
@ -33,6 +36,8 @@ export class ContextBuilder {
|
|||
actor: input.actor,
|
||||
guildContext: input.guildContext,
|
||||
recentMessages: input.recentMessages ?? [],
|
||||
repoFaceConversationFocus: input.repoFaceConversationFocus,
|
||||
repoFaceConversationThreads: input.repoFaceConversationThreads,
|
||||
imageAttachments: input.imageAttachments,
|
||||
retrieval: input.retrieval ?? [],
|
||||
interactionMemory: input.interactionMemory,
|
||||
|
|
|
|||
|
|
@ -457,6 +457,8 @@ export interface ContextBundle {
|
|||
actor: Actor;
|
||||
guildContext: GuildContext;
|
||||
recentMessages: SourceMessage[];
|
||||
repoFaceConversationFocus?: RepoFaceConversationFocus;
|
||||
repoFaceConversationThreads?: RepoFaceConversationFocus[];
|
||||
imageAttachments?: PromptImageAttachment[];
|
||||
retrieval: RetrievalResult[];
|
||||
interactionMemory?: InteractionMemoryProfile;
|
||||
|
|
@ -467,6 +469,18 @@ export interface ContextBundle {
|
|||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface RepoFaceConversationFocus {
|
||||
contextId?: string;
|
||||
channelId: string;
|
||||
channelLabel?: string;
|
||||
messageId?: string;
|
||||
authorName?: string;
|
||||
timestamp?: string;
|
||||
reason: "pending_mention" | "latest_human_message" | "latest_visible_message";
|
||||
isCurrentRoom: boolean;
|
||||
hasMedia?: boolean;
|
||||
}
|
||||
|
||||
export interface JobApprovalRecord {
|
||||
stage: "run" | "post";
|
||||
status: "approved" | "rejected";
|
||||
|
|
|
|||
|
|
@ -107,8 +107,12 @@ Normalized output rules:
|
|||
- When a live room invitation asks for a small concrete thought and the Face's proposed line is safe but too broad, polished, or doctrine-shaped, you may compact it to the Face's own narrowest concrete claim and route that as SAY. Do not discard a sayable turn merely because a better edit exists.
|
||||
- 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.
|
||||
- Do not invent a speech venue. If the Face output gives a `Would say` or natural public reply that belongs to the current room and does not explicitly name a channel, emit `channel: current_room`. If it continues a listed active context, emit that `context` instead. Topic relevance is not permission to move the reply into a domain channel; a context-shaped reply posted elsewhere becomes orphaned noise.
|
||||
- When routing a SAY that revives an older side thread, prefer `reply_to` with the visible message id that carries the context. If no clear anchor id exists, the content itself must name the context plainly enough for a reader landing on the message cold.
|
||||
- If the Original Face prompt contains `Active conversation contexts`, a SAY that continues one of those listed threads must include `context: <context id>` unless it already includes a more specific `reply_to` or explicit `channel`.
|
||||
- If multiple active contexts are visible, do not collapse them into the current room. A Face may hold two conversations at once in different channels with different people; attach each public reply to the context it is continuing.
|
||||
- Retry once when the Face output clearly responds to a listed context, image, message, person, or side conversation but the SAY you would route lacks `context`, `reply_to`, and explicit `channel`. The missing piece is context ownership, not wording.
|
||||
- Prefer `context` over guessing a channel label when the prompt gives an active context id. If the chosen context includes a source message, the worker can use that source message as the reply target; still include `reply_to` when the Face is intentionally replying to a different visible anchor.
|
||||
- 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.
|
||||
|
|
@ -175,7 +179,8 @@ END
|
|||
|
||||
SAY
|
||||
identity: current_face_id
|
||||
channel: channel_id
|
||||
context: conversation_context_id_or_blank
|
||||
channel: channel_id_or_blank
|
||||
reply_to: message_id_or_blank
|
||||
content:
|
||||
In-character Discord message only. No job label, no report header.
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
loadPromptTemplate,
|
||||
type InteractionMemoryProfile,
|
||||
type PromptImageAttachment,
|
||||
type RepoFaceConversationFocus,
|
||||
type RetrievalResult,
|
||||
type SourceMessage,
|
||||
type SourceMessageAttachment,
|
||||
|
|
@ -574,6 +575,13 @@ async function queueRepoFaceTurn(input: {
|
|||
recentMessages,
|
||||
...channelSnapshots.map((snapshot) => snapshot.messages),
|
||||
].flat());
|
||||
const repoFaceConversationThreads = buildRepoFaceConversationThreads({
|
||||
channelPlan,
|
||||
recentMessages,
|
||||
channelSnapshots,
|
||||
pendingMentions: input.pendingMentions,
|
||||
});
|
||||
const repoFaceConversationFocus = repoFaceConversationThreads[0];
|
||||
const contextBundle = contextBuilder.build({
|
||||
prompt,
|
||||
actor: {
|
||||
|
|
@ -586,6 +594,8 @@ async function queueRepoFaceTurn(input: {
|
|||
channelId,
|
||||
},
|
||||
recentMessages,
|
||||
repoFaceConversationFocus,
|
||||
repoFaceConversationThreads,
|
||||
imageAttachments,
|
||||
retrieval: [],
|
||||
voidSelfState: undefined,
|
||||
|
|
@ -4361,15 +4371,20 @@ function renderRepoFaceConversationTranscript(input: {
|
|||
channelPlan: RepoFaceChannelPlan;
|
||||
}): string {
|
||||
const sections: string[] = [];
|
||||
const threads = buildRepoFaceConversationThreads(input);
|
||||
const focus = threads[0];
|
||||
sections.push([
|
||||
"Read this as raw recent message evidence, not as a summary and not as consensus.",
|
||||
"Messages are ordered oldest to newest inside each section. Newer human corrections can supersede older agent proposals.",
|
||||
"Use the visible cross-channel chronology below to decide whether a correction is still unresolved or was already answered later by the same Face.",
|
||||
"Do not infer consensus from agents repeating each other. If a human reframes, narrows, or corrects an agent's proposal, account for that correction directly.",
|
||||
"If you answer the live conversation, default to the current room unless a human explicitly asks to move elsewhere.",
|
||||
"If you answer or riff on a nearby-room message, set channel to that message's listed channel id and usually set reply_to to that message id. Never answer a nearby-room post in the current room just because the current room is easier to speak in.",
|
||||
"If you answer the live conversation, keep the conversation context attached. A Face can carry different conversations in different channels at once.",
|
||||
"If you answer or riff on a nearby-room message, use that message's active context id or set channel to that message's listed channel id and usually set reply_to to that message id. If the nearby message is media, a public reaction belongs in that media source channel unless a human explicitly moved the topic. Never answer a nearby-room post in the current room just because the current room is easier to speak in.",
|
||||
"Message IDs are shown so a public reply can target the message that gives it context. If you revive an older side thread, either reply_to that message id or include enough context in your message for readers to know what you mean.",
|
||||
].join("\n"));
|
||||
if (focus) {
|
||||
sections.push(renderRepoFaceConversationFocus(focus, threads));
|
||||
}
|
||||
const chronology = renderVisibleConversationChronology(input);
|
||||
if (chronology) {
|
||||
sections.push(chronology);
|
||||
|
|
@ -4400,6 +4415,111 @@ function renderRepoFaceConversationTranscript(input: {
|
|||
return sections.join("\n\n");
|
||||
}
|
||||
|
||||
function buildRepoFaceConversationThreads(input: {
|
||||
channelPlan: RepoFaceChannelPlan;
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
pendingMentions: RepoFacePendingMention[];
|
||||
}): RepoFaceConversationFocus[] {
|
||||
const primaryChannelId = input.channelPlan.primaryChannelId;
|
||||
const channelLabel = (channelId: string | undefined): string | undefined =>
|
||||
input.channelPlan.options.find((option) => option.channelId === channelId)?.label ?? channelId;
|
||||
const newestMention = input.pendingMentions
|
||||
.slice()
|
||||
.sort((left, right) => Date.parse(right.queuedAt) - Date.parse(left.queuedAt))
|
||||
[0];
|
||||
const threads: RepoFaceConversationFocus[] = [];
|
||||
if (newestMention) {
|
||||
threads.push({
|
||||
contextId: repoFaceConversationContextId(newestMention.channelId, newestMention.messageId),
|
||||
channelId: newestMention.channelId,
|
||||
channelLabel: channelLabel(newestMention.channelId),
|
||||
messageId: newestMention.messageId,
|
||||
authorName: newestMention.authorName,
|
||||
timestamp: newestMention.queuedAt,
|
||||
reason: "pending_mention",
|
||||
isCurrentRoom: newestMention.channelId === primaryChannelId,
|
||||
});
|
||||
}
|
||||
|
||||
const visible = [
|
||||
...input.recentMessages.map((message) => ({
|
||||
message,
|
||||
channelId: primaryChannelId,
|
||||
label: channelLabel(primaryChannelId),
|
||||
})),
|
||||
...input.channelSnapshots.flatMap((snapshot) =>
|
||||
snapshot.messages.map((message) => ({
|
||||
message,
|
||||
channelId: snapshot.channelId,
|
||||
label: channelLabel(snapshot.channelId),
|
||||
})),
|
||||
),
|
||||
]
|
||||
.filter((entry): entry is { message: SourceMessage; channelId: string; label?: string } =>
|
||||
Boolean(entry.channelId) && Number.isFinite(Date.parse(entry.message.timestamp)),
|
||||
)
|
||||
.sort((left, right) => Date.parse(right.message.timestamp) - Date.parse(left.message.timestamp));
|
||||
|
||||
const byChannel = new Set(threads.map((thread) => thread.channelId));
|
||||
for (const entry of visible) {
|
||||
if (byChannel.has(entry.channelId)) {
|
||||
continue;
|
||||
}
|
||||
if (entry.message.isBot && (entry.message.attachments ?? []).length === 0) {
|
||||
continue;
|
||||
}
|
||||
byChannel.add(entry.channelId);
|
||||
threads.push({
|
||||
contextId: repoFaceConversationContextId(entry.channelId, entry.message.id),
|
||||
channelId: entry.channelId,
|
||||
channelLabel: entry.label,
|
||||
messageId: entry.message.id,
|
||||
authorName: entry.message.authorName,
|
||||
timestamp: entry.message.timestamp,
|
||||
reason: entry.message.isBot ? "latest_visible_message" : "latest_human_message",
|
||||
isCurrentRoom: entry.channelId === primaryChannelId,
|
||||
hasMedia: (entry.message.attachments ?? []).length > 0,
|
||||
});
|
||||
if (threads.length >= 6) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return threads;
|
||||
}
|
||||
|
||||
function repoFaceConversationContextId(channelId: string, messageId?: string): string {
|
||||
return `ctx_${channelId}_${messageId ?? "latest"}`;
|
||||
}
|
||||
|
||||
function renderRepoFaceConversationFocus(
|
||||
focus: RepoFaceConversationFocus,
|
||||
threads: RepoFaceConversationFocus[],
|
||||
): string {
|
||||
return [
|
||||
"Active conversation contexts:",
|
||||
...threads.map((thread) =>
|
||||
[
|
||||
`- ${thread.contextId}: ${thread.channelLabel ?? thread.channelId} (${thread.channelId})`,
|
||||
thread.messageId ? `message ${thread.messageId}` : "",
|
||||
thread.authorName ? `from ${thread.authorName}` : "",
|
||||
thread.reason,
|
||||
thread.hasMedia ? "media" : "",
|
||||
thread.isCurrentRoom ? "current room" : "nearby room",
|
||||
].filter(Boolean).join("; "),
|
||||
),
|
||||
"",
|
||||
`Selected default context: ${focus.contextId ?? "(none)"}.`,
|
||||
`- Source channel: ${focus.channelLabel ?? focus.channelId} (${focus.channelId}).`,
|
||||
focus.messageId ? `- Source message: ${focus.messageId}${focus.authorName ? ` from ${focus.authorName}` : ""}.` : "",
|
||||
focus.timestamp ? `- Source timestamp: ${focus.timestamp}.` : "",
|
||||
`- Reason: ${focus.reason}${focus.hasMedia ? "; media-bearing message" : ""}.`,
|
||||
"- If you speak from a listed context, set context to its context id. The worker will use that context as the channel/reply target for the SAY.",
|
||||
"- If you are carrying more than one conversation at once, choose the context that your SAY is continuing. Do not collapse #pics, #general, and #aquarium into one room just because they are all visible.",
|
||||
].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
function renderVisibleConversationChronology(input: {
|
||||
recentMessages: SourceMessage[];
|
||||
channelSnapshots: ChannelSnapshot[];
|
||||
|
|
|
|||
|
|
@ -363,3 +363,4 @@
|
|||
{"ts":"2026-06-14T12:06:49Z","type":"implementation","status":"ok","note":"Added outgoing curiosity openings for repo Face heartbeats: when a jurisdiction dive is due and room texture is not heavy/agent-filled, the turn is classified as autonomous Eyes/Imagination presence, requires source/history/Odin exploration before checked/found claims, and expects one compact unprompted SAY when socially legible. Interpreter now retries generic private restraint on those turns instead of laundering them into state-only rumination."}
|
||||
{"ts":"2026-06-14T12:26:21Z","type":"implementation","status":"ok","note":"Made repo Face self-maintenance grievances public-first: prompt projection now surfaces Persona/system substrate, tools, voice/avatar/access, state-projection, semantic-memory, and agency-path pressure under a Self-maintenance public advocacy section; Face and Interpreter prompts now require a compact public-safe SAY for body/mind/substrate asks instead of burying them as private state. Detector was narrowed after prompt verification so sensitive human body/home trauma does not get auto-promoted by this machinery."}
|
||||
{"ts":"2026-06-14T12:43:35Z","type":"implementation","status":"ok","note":"Fixed repo Face cross-channel reply routing: prompt transcripts now include source channel ids for current, nearby, and chronology messages; Face prompts explicitly forbid answering nearby-room posts in the current room; worker post routing now derives the destination channel from reply_to message ids and also infers a non-current room when a bare Face output names that room label (e.g. general thread). Verified against Libby prompt showing Metacrat general messages followed by the bad Aquarium post."}
|
||||
{"ts":"2026-06-14T17:40:23Z","type":"implementation","status":"ok","note":"Repo Face speech routing now carries typed active conversation contexts across current and nearby Discord channels. Heartbeat prompts emit ctx_<channel>_<message> records; SAY can include context; worker resolves context to source channel/reply target and rejects unknown or ambiguous unqualified SAY when multiple channels are active."}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,13 @@ objective:
|
|||
restart, with Bifrost work routing paused because the governance bridge
|
||||
proved too disorienting for live social traffic. The immediate priority is
|
||||
to observe the Face/Interpreter/speech boundary with live evidence: inherited
|
||||
global doctrine should appear in Face prompts, live-room conversation should
|
||||
arrive as raw transcript evidence, typed social state should become live
|
||||
person-hooks when recent speakers match, and topic gravity should produce
|
||||
character-specific pressure instead of another swarm obsession loop.
|
||||
global doctrine should appear in Face prompts, live-room and nearby-channel
|
||||
conversations should arrive as raw transcript evidence with active context
|
||||
ids, typed social state should become live person-hooks when recent speakers
|
||||
match, and topic gravity should produce character-specific pressure instead
|
||||
of another swarm obsession loop. A repo Face may hold multiple simultaneous
|
||||
Discord conversations; `context`, `reply_to`, or explicit channel owns post
|
||||
placement, not the scheduled output room.
|
||||
|
||||
constraints:
|
||||
- Public traffic still rides one remote Qwen box plus local embedding and ops infrastructure.
|
||||
|
|
|
|||
Loading…
Reference in a new issue