Add repo Face self-transcript tools
This commit is contained in:
parent
0e2fc95cee
commit
9d5e1fc803
5 changed files with 499 additions and 1 deletions
|
|
@ -43,6 +43,23 @@ export const repoFaceStateInputSchema = {
|
|||
identity: z.string().min(1),
|
||||
};
|
||||
|
||||
export const repoFaceSelfTranscriptsListInputSchema = {
|
||||
identity: z.string().min(1),
|
||||
limit: z.number().int().min(1).max(20).optional(),
|
||||
};
|
||||
|
||||
export const repoFaceSelfTranscriptInputSchema = {
|
||||
identity: z.string().min(1),
|
||||
jobId: z.string().min(1),
|
||||
includeRaw: z.boolean().optional(),
|
||||
};
|
||||
|
||||
export const repoFaceSelfTranscriptSearchInputSchema = {
|
||||
identity: z.string().min(1),
|
||||
query: z.string().min(1).max(240),
|
||||
limit: z.number().int().min(1).max(20).optional(),
|
||||
};
|
||||
|
||||
export const applyRepoFaceStateOperationInputSchema = {
|
||||
identity: z.string().min(1),
|
||||
operation: z.record(z.unknown()),
|
||||
|
|
@ -127,6 +144,23 @@ export interface RepoFaceStateArgs {
|
|||
identity: string;
|
||||
}
|
||||
|
||||
export interface RepoFaceSelfTranscriptsListArgs {
|
||||
identity: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface RepoFaceSelfTranscriptArgs {
|
||||
identity: string;
|
||||
jobId: string;
|
||||
includeRaw?: boolean;
|
||||
}
|
||||
|
||||
export interface RepoFaceSelfTranscriptSearchArgs {
|
||||
identity: string;
|
||||
query: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface ApplyRepoFaceStateOperationArgs {
|
||||
identity: string;
|
||||
operation: Record<string, unknown>;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { type CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import crypto from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import net from "node:net";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
import { DEFAULT_RETRIEVAL_RESULT_LIMIT } from "@voidbot/shared";
|
||||
import { searchHistoryWithArchiveFallback } from "@voidbot/rag";
|
||||
|
|
@ -34,6 +36,9 @@ import {
|
|||
type ApplyRepoFaceStateOperationArgs,
|
||||
type PostDiscordMessageArgs,
|
||||
type PostRepoIdentityMessageArgs,
|
||||
type RepoFaceSelfTranscriptArgs,
|
||||
type RepoFaceSelfTranscriptSearchArgs,
|
||||
type RepoFaceSelfTranscriptsListArgs,
|
||||
type RepoFaceStateArgs,
|
||||
type RuntimeInfoArgs,
|
||||
type SearchHistoryArgs,
|
||||
|
|
@ -51,6 +56,9 @@ import {
|
|||
odinSurfaceInputSchema,
|
||||
postDiscordMessageInputSchema,
|
||||
postRepoIdentityMessageInputSchema,
|
||||
repoFaceSelfTranscriptInputSchema,
|
||||
repoFaceSelfTranscriptSearchInputSchema,
|
||||
repoFaceSelfTranscriptsListInputSchema,
|
||||
repoFaceStateInputSchema,
|
||||
renderJsonBlock,
|
||||
runtimeInfoInputSchema,
|
||||
|
|
@ -940,6 +948,140 @@ export function registerVoidbotTools(
|
|||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"list_repo_face_self_transcripts",
|
||||
{
|
||||
title: "List Repo Face Self Transcripts",
|
||||
description:
|
||||
"List recent read-only Projector / Persona / Interpreter / Delivery witness packets for a registered repo Face. Use this when the Face wants to inspect how recent turns were shaped without injecting those transcripts into every prompt.",
|
||||
inputSchema: repoFaceSelfTranscriptsListInputSchema,
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async (input: RepoFaceSelfTranscriptsListArgs): Promise<CallToolResult> => {
|
||||
const resolved = await resolveRepoIdentityForTool(context, input.identity);
|
||||
|
||||
if (!resolved.identity) {
|
||||
return resolved.error;
|
||||
}
|
||||
|
||||
const packets = await listRepoFaceSelfTranscriptPackets(context, resolved.identity, input.limit ?? 5);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: renderJsonBlock({
|
||||
identity: identityForToolResult(resolved.identity, resolved.face),
|
||||
count: packets.length,
|
||||
packets,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
identity: identityForToolResult(resolved.identity, resolved.face),
|
||||
count: packets.length,
|
||||
packets,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"read_repo_face_self_transcript",
|
||||
{
|
||||
title: "Read Repo Face Self Transcript",
|
||||
description:
|
||||
"Read one read-only self-transcript witness packet for a registered repo Face: what was projected, what the Persona wrote, what the Interpreter routed, and what delivery receipts exist.",
|
||||
inputSchema: repoFaceSelfTranscriptInputSchema,
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async (input: RepoFaceSelfTranscriptArgs): Promise<CallToolResult> => {
|
||||
const resolved = await resolveRepoIdentityForTool(context, input.identity);
|
||||
|
||||
if (!resolved.identity) {
|
||||
return resolved.error;
|
||||
}
|
||||
|
||||
const packet = await readRepoFaceSelfTranscriptPacket(
|
||||
context,
|
||||
resolved.identity,
|
||||
input.jobId,
|
||||
Boolean(input.includeRaw),
|
||||
);
|
||||
if (!packet) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `No self-transcript packet for ${resolved.identity.id} matched job ${input.jobId}.`,
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
found: false,
|
||||
identity: identityForToolResult(resolved.identity, resolved.face),
|
||||
jobId: input.jobId,
|
||||
},
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: renderJsonBlock(packet),
|
||||
},
|
||||
],
|
||||
structuredContent: packet,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"search_repo_face_self_transcripts",
|
||||
{
|
||||
title: "Search Repo Face Self Transcripts",
|
||||
description:
|
||||
"Search recent read-only self-transcript witness packets for a registered repo Face. This searches Projector, Persona, Interpreter, and delivery preview text.",
|
||||
inputSchema: repoFaceSelfTranscriptSearchInputSchema,
|
||||
annotations: READ_ONLY_ANNOTATIONS,
|
||||
},
|
||||
async (input: RepoFaceSelfTranscriptSearchArgs): Promise<CallToolResult> => {
|
||||
const resolved = await resolveRepoIdentityForTool(context, input.identity);
|
||||
|
||||
if (!resolved.identity) {
|
||||
return resolved.error;
|
||||
}
|
||||
|
||||
const packets = await searchRepoFaceSelfTranscriptPackets(
|
||||
context,
|
||||
resolved.identity,
|
||||
input.query,
|
||||
input.limit ?? 5,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: renderJsonBlock({
|
||||
identity: identityForToolResult(resolved.identity, resolved.face),
|
||||
query: input.query,
|
||||
count: packets.length,
|
||||
packets,
|
||||
}),
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
identity: identityForToolResult(resolved.identity, resolved.face),
|
||||
query: input.query,
|
||||
count: packets.length,
|
||||
packets,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerIfAllowed(
|
||||
"apply_repo_face_state_operation",
|
||||
{
|
||||
|
|
@ -1091,6 +1233,323 @@ async function resolveRepoIdentityForTool(
|
|||
};
|
||||
}
|
||||
|
||||
interface RepoFaceModelOutputRecord {
|
||||
jobId: string;
|
||||
command: string;
|
||||
promptMarker?: string | null;
|
||||
loggedAt?: string;
|
||||
startedAt?: string;
|
||||
finishedAt?: string;
|
||||
model?: string;
|
||||
finalMessage?: string | null;
|
||||
artifactRefs?: Record<string, string>;
|
||||
usage?: unknown;
|
||||
exitCode?: number | null;
|
||||
timedOut?: boolean;
|
||||
}
|
||||
|
||||
interface RepoFaceDeliveryReceipt {
|
||||
receiptKey?: string;
|
||||
sentAt?: string;
|
||||
channelId?: string;
|
||||
replyToMessageId?: string;
|
||||
messageId?: string;
|
||||
transport?: string;
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
async function listRepoFaceSelfTranscriptPackets(
|
||||
context: VoidbotMcpContext,
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>,
|
||||
limit: number,
|
||||
): Promise<Array<Record<string, unknown>>> {
|
||||
const logs = await readRepoFaceModelOutputLogs(context);
|
||||
const grouped = await groupRepoFaceTurnLogsForIdentity(context, identity, logs);
|
||||
const receipts = await readRepoFaceDeliveryReceipts(context, identity);
|
||||
return grouped
|
||||
.slice(0, limit)
|
||||
.map((group) => summarizeRepoFaceTranscriptGroup(identity, group, logs, receipts, false));
|
||||
}
|
||||
|
||||
async function readRepoFaceSelfTranscriptPacket(
|
||||
context: VoidbotMcpContext,
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>,
|
||||
jobId: string,
|
||||
includeRaw: boolean,
|
||||
): Promise<Record<string, unknown> | undefined> {
|
||||
const logs = await readRepoFaceModelOutputLogs(context);
|
||||
const grouped = await groupRepoFaceTurnLogsForIdentity(context, identity, logs);
|
||||
const group = grouped.find((entry) => entry.jobId === jobId);
|
||||
if (!group) {
|
||||
return undefined;
|
||||
}
|
||||
const receipts = await readRepoFaceDeliveryReceipts(context, identity);
|
||||
return summarizeRepoFaceTranscriptGroup(identity, group, logs, receipts, includeRaw);
|
||||
}
|
||||
|
||||
async function searchRepoFaceSelfTranscriptPackets(
|
||||
context: VoidbotMcpContext,
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>,
|
||||
query: string,
|
||||
limit: number,
|
||||
): Promise<Array<Record<string, unknown>>> {
|
||||
const logs = await readRepoFaceModelOutputLogs(context);
|
||||
const grouped = await groupRepoFaceTurnLogsForIdentity(context, identity, logs);
|
||||
const receipts = await readRepoFaceDeliveryReceipts(context, identity);
|
||||
const needle = query.toLowerCase();
|
||||
return grouped
|
||||
.map((group) => summarizeRepoFaceTranscriptGroup(identity, group, logs, receipts, false))
|
||||
.filter((packet) => JSON.stringify(packet).toLowerCase().includes(needle))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
async function readRepoFaceModelOutputLogs(context: VoidbotMcpContext): Promise<RepoFaceModelOutputRecord[]> {
|
||||
const logPath = join(context.config.storageRoot, "logs", "model-outputs.jsonl");
|
||||
let content = "";
|
||||
try {
|
||||
content = await readFile(logPath, "utf8");
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
return content
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.trim().length > 0)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line) as RepoFaceModelOutputRecord;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter((record): record is RepoFaceModelOutputRecord => Boolean(record))
|
||||
.filter((record) =>
|
||||
record.command === "repo-face-rumination" ||
|
||||
record.command === "repo-face-state-projector"
|
||||
);
|
||||
}
|
||||
|
||||
async function groupRepoFaceTurnLogsForIdentity(
|
||||
context: VoidbotMcpContext,
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>,
|
||||
logs: RepoFaceModelOutputRecord[],
|
||||
): Promise<Array<{
|
||||
jobId: string;
|
||||
requestPath?: string;
|
||||
requestExcerpt?: string;
|
||||
persona: RepoFaceModelOutputRecord[];
|
||||
interpreter: RepoFaceModelOutputRecord[];
|
||||
firstAt?: string;
|
||||
lastAt?: string;
|
||||
}>> {
|
||||
const byJob = new Map<string, RepoFaceModelOutputRecord[]>();
|
||||
for (const record of logs.filter((entry) => entry.command === "repo-face-rumination")) {
|
||||
if (!record.jobId) {
|
||||
continue;
|
||||
}
|
||||
const records = byJob.get(record.jobId) ?? [];
|
||||
records.push(record);
|
||||
byJob.set(record.jobId, records);
|
||||
}
|
||||
|
||||
const groups: Array<{
|
||||
jobId: string;
|
||||
requestPath?: string;
|
||||
requestExcerpt?: string;
|
||||
persona: RepoFaceModelOutputRecord[];
|
||||
interpreter: RepoFaceModelOutputRecord[];
|
||||
firstAt?: string;
|
||||
lastAt?: string;
|
||||
}> = [];
|
||||
|
||||
for (const [jobId, records] of byJob) {
|
||||
const requestPath = resolve(context.config.storageRoot, "artifacts", jobId, "request.md");
|
||||
const request = await readOptionalText(requestPath);
|
||||
if (!request || !requestBelongsToRepoFace(request, identity)) {
|
||||
continue;
|
||||
}
|
||||
const sorted = records.slice().sort(compareModelOutputRecords);
|
||||
groups.push({
|
||||
jobId,
|
||||
requestPath,
|
||||
requestExcerpt: excerptRequestWitness(request),
|
||||
persona: sorted.filter((record) => record.promptMarker === "character-turn"),
|
||||
interpreter: sorted.filter((record) => record.promptMarker === "repo-face-turn-interpreter"),
|
||||
firstAt: sorted[0]?.startedAt ?? sorted[0]?.loggedAt,
|
||||
lastAt: sorted.at(-1)?.finishedAt ?? sorted.at(-1)?.loggedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return groups.sort((left, right) =>
|
||||
Date.parse(right.firstAt ?? "") - Date.parse(left.firstAt ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
function summarizeRepoFaceTranscriptGroup(
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>,
|
||||
group: {
|
||||
jobId: string;
|
||||
requestPath?: string;
|
||||
requestExcerpt?: string;
|
||||
persona: RepoFaceModelOutputRecord[];
|
||||
interpreter: RepoFaceModelOutputRecord[];
|
||||
firstAt?: string;
|
||||
lastAt?: string;
|
||||
},
|
||||
logs: RepoFaceModelOutputRecord[],
|
||||
receipts: RepoFaceDeliveryReceipt[],
|
||||
includeRaw: boolean,
|
||||
): Record<string, unknown> {
|
||||
const projector = findNearestProjectorRecord(identity, logs, group.firstAt);
|
||||
const delivered = receipts.filter((receipt) => receiptInTurnWindow(receipt, group.firstAt, group.lastAt));
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
identity: {
|
||||
id: identity.id,
|
||||
displayName: identity.displayName,
|
||||
repoName: identity.repoName,
|
||||
},
|
||||
jobId: group.jobId,
|
||||
startedAt: group.firstAt ?? null,
|
||||
finishedAt: group.lastAt ?? null,
|
||||
seen: {
|
||||
requestPath: group.requestPath ?? null,
|
||||
requestExcerpt: group.requestExcerpt ?? null,
|
||||
},
|
||||
projected: projector
|
||||
? modelRecordSummary(projector, includeRaw)
|
||||
: null,
|
||||
persona: group.persona.map((record) => modelRecordSummary(record, includeRaw)),
|
||||
interpreted: group.interpreter.map((record) => modelRecordSummary(record, includeRaw)),
|
||||
delivered: delivered.map((receipt) => ({
|
||||
sentAt: receipt.sentAt ?? null,
|
||||
channelId: receipt.channelId ?? null,
|
||||
replyToMessageId: receipt.replyToMessageId ?? null,
|
||||
messageId: receipt.messageId ?? receipt.receiptKey?.split(":").at(-1) ?? null,
|
||||
transport: receipt.transport ?? null,
|
||||
preview: receipt.preview ?? null,
|
||||
})),
|
||||
artifactRefs: {
|
||||
persona: group.persona.map((record) => record.artifactRefs ?? {}),
|
||||
interpreter: group.interpreter.map((record) => record.artifactRefs ?? {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function modelRecordSummary(record: RepoFaceModelOutputRecord, includeRaw: boolean): Record<string, unknown> {
|
||||
return {
|
||||
jobId: record.jobId,
|
||||
command: record.command,
|
||||
promptMarker: record.promptMarker ?? null,
|
||||
model: record.model ?? null,
|
||||
loggedAt: record.loggedAt ?? null,
|
||||
startedAt: record.startedAt ?? null,
|
||||
finishedAt: record.finishedAt ?? null,
|
||||
exitCode: record.exitCode ?? null,
|
||||
timedOut: record.timedOut ?? false,
|
||||
usage: record.usage ?? null,
|
||||
finalMessage: includeRaw
|
||||
? record.finalMessage ?? null
|
||||
: collapseText(record.finalMessage ?? "", 900),
|
||||
artifactRefs: record.artifactRefs ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
function findNearestProjectorRecord(
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>,
|
||||
logs: RepoFaceModelOutputRecord[],
|
||||
before: string | undefined,
|
||||
): RepoFaceModelOutputRecord | undefined {
|
||||
const beforeMs = Date.parse(before ?? "");
|
||||
return logs
|
||||
.filter((record) =>
|
||||
record.command === "repo-face-state-projector" &&
|
||||
record.jobId?.startsWith(`state-projector:${identity.id}:`)
|
||||
)
|
||||
.filter((record) => {
|
||||
if (!Number.isFinite(beforeMs)) {
|
||||
return true;
|
||||
}
|
||||
const loggedAtMs = Date.parse(record.loggedAt ?? record.finishedAt ?? record.startedAt ?? "");
|
||||
return Number.isFinite(loggedAtMs) && loggedAtMs <= beforeMs;
|
||||
})
|
||||
.sort(compareModelOutputRecords)
|
||||
.at(-1);
|
||||
}
|
||||
|
||||
async function readRepoFaceDeliveryReceipts(
|
||||
context: VoidbotMcpContext,
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>,
|
||||
): Promise<RepoFaceDeliveryReceipt[]> {
|
||||
const typedState = await loadVoidSelfStateTypedDocuments({
|
||||
canonicalPath: resolveRepoFaceStatePath(identity, context.config.storageRoot),
|
||||
identity: {
|
||||
agentId: identity.id,
|
||||
publicName: identity.displayName,
|
||||
publicDescription: identity.description,
|
||||
},
|
||||
});
|
||||
const receipts = (typedState.speechReceipts as { recentReceipts?: unknown[] }).recentReceipts ?? [];
|
||||
return receipts.filter((entry): entry is RepoFaceDeliveryReceipt =>
|
||||
Boolean(entry) && typeof entry === "object"
|
||||
);
|
||||
}
|
||||
|
||||
function receiptInTurnWindow(receipt: RepoFaceDeliveryReceipt, startedAt: string | undefined, finishedAt: string | undefined): boolean {
|
||||
const sentAtMs = Date.parse(receipt.sentAt ?? "");
|
||||
const startMs = Date.parse(startedAt ?? "");
|
||||
const finishMs = Date.parse(finishedAt ?? "");
|
||||
if (!Number.isFinite(sentAtMs)) {
|
||||
return false;
|
||||
}
|
||||
const lower = Number.isFinite(startMs) ? startMs - 60_000 : -Infinity;
|
||||
const upper = Number.isFinite(finishMs) ? finishMs + 5 * 60_000 : Infinity;
|
||||
return sentAtMs >= lower && sentAtMs <= upper;
|
||||
}
|
||||
|
||||
function requestBelongsToRepoFace(
|
||||
request: string,
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>,
|
||||
): boolean {
|
||||
const firstLine = request.split(/\r?\n/).find((line) => line.startsWith("Be "));
|
||||
return Boolean(
|
||||
firstLine &&
|
||||
firstLine.includes(`Be ${identity.displayName} `) &&
|
||||
firstLine.includes(` around ${identity.repoName}.`),
|
||||
);
|
||||
}
|
||||
|
||||
function excerptRequestWitness(request: string): string {
|
||||
const markers = [
|
||||
"Recent conversation transcript:",
|
||||
"Fresh projected state for this turn:",
|
||||
"Current room (",
|
||||
"Visible cross-channel chronology",
|
||||
];
|
||||
const index = markers
|
||||
.map((marker) => request.indexOf(marker))
|
||||
.filter((entry) => entry >= 0)
|
||||
.sort((left, right) => left - right)[0] ?? 0;
|
||||
return collapseText(request.slice(index), 1800);
|
||||
}
|
||||
|
||||
async function readOptionalText(path: string): Promise<string | undefined> {
|
||||
try {
|
||||
return await readFile(path, "utf8");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function compareModelOutputRecords(left: RepoFaceModelOutputRecord, right: RepoFaceModelOutputRecord): number {
|
||||
return Date.parse(left.startedAt ?? left.loggedAt ?? "") - Date.parse(right.startedAt ?? right.loggedAt ?? "");
|
||||
}
|
||||
|
||||
function collapseText(value: string, maxLength: number): string {
|
||||
const collapsed = value.replace(/\s+/g, " ").trim();
|
||||
return collapsed.length > maxLength ? `${collapsed.slice(0, Math.max(0, maxLength - 3))}...` : collapsed;
|
||||
}
|
||||
|
||||
async function recordRepoIdentityDeliveryReceipt(input: {
|
||||
context: VoidbotMcpContext;
|
||||
identity: NonNullable<ReturnType<typeof findRepoDiscordIdentity>>;
|
||||
|
|
|
|||
|
|
@ -556,6 +556,9 @@ function restrictMcpServersToRepoFaceExploration(
|
|||
"get_odin_surface",
|
||||
"load_odin_interface_context",
|
||||
"invoke_odin_interface_command",
|
||||
"list_repo_face_self_transcripts",
|
||||
"read_repo_face_self_transcript",
|
||||
"search_repo_face_self_transcripts",
|
||||
].join(",");
|
||||
|
||||
return servers.map((server) => ({
|
||||
|
|
|
|||
|
|
@ -338,3 +338,4 @@
|
|||
{"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."}
|
||||
{"ts":"2026-06-04T16:32:15Z","type":"implementation","status":"ok","note":"Extracted repo Face Colossus/Odin doctrine and Verse poem into prompts/repo-face-colossus-odin-doctrine.prompt.md; repo-face-turn.prompt.md now receives it as a rendered prompt partial from buildHeartbeatPrompt."}
|
||||
{"ts":"2026-06-04T16:58:28Z","type":"bugfix","status":"ok","note":"Fixed repo Face live-channel ownership exposed by Weksa: CTB turns now fetch allowed channel snapshots before selecting the current room, choose pending mentions first or the freshest human message across allowed channels, store that channel as guildContext/outputChannelId, and carry a default replyToMessageId for worker speech fallback when the Interpreter does not set reply_to."}
|
||||
{"ts":"2026-06-04T17:32:36Z","type":"implementation","status":"ok","note":"Added read-only repo Face self-transcript MCP tools: list/read/search Projector-Persona-Interpreter-Delivery witness packets from model-output logs, job artifacts, and typed Face delivery receipts. Added the tools to the restricted Face child MCP allowlist and restarted the worker directly as PID 26720 after the full stack preflight blocked on the remote Ollama endpoint."}
|
||||
|
|
|
|||
|
|
@ -206,6 +206,7 @@ landed_baseline:
|
|||
- The worker MCP surface is now split into a thin stdio entrypoint plus dedicated bootstrap, resource, tool, Discord-notify, and shared-helper modules instead of one swollen server file.
|
||||
- The MCP Discord surface now includes registered repo identities: `list_repo_discord_identities` exposes the role-backed address book, and `post_repo_identity_message` posts with the registered display name/avatar through the existing webhook pipe while enforcing channel allow-lists.
|
||||
- The MCP Face surface now exposes `read_repo_face_state` and `apply_repo_face_state_operation`, reusing the typed self-state operation service against each repo identity's own Face `.cc` file instead of creating editable JSON projections.
|
||||
- The MCP Face surface also exposes read-only self-transcript witness tools: `list_repo_face_self_transcripts`, `read_repo_face_self_transcript`, and `search_repo_face_self_transcripts`. They do not inject transcripts into every Face turn and do not mutate state; they let a Face intentionally inspect existing Projector/Persona/Interpreter/Delivery witnesses from `.voidbot/logs/model-outputs.jsonl`, `.voidbot/artifacts/<jobId>/`, and typed Face delivery receipts.
|
||||
- Repo Face birth initialization is detached and observable: VoidBot writes repo-local status/log paths before launching `scripts/run-repo-face-birth.mjs`, which runs EpiphanyAgent's `epiphany-repo-personality scout` followed by `epiphany-repo-birth-runner`.
|
||||
- Repo Face heartbeats now have a Ghostlight-style initiative scheduler: `scripts/run-repo-face-heartbeats.ts` reconciles the registered identities, projects Epiphany/Face doctrine into prompts, tracks staggered next-ready times and active load, and queues ready Faces as approved `repo-face-rumination` owner-Codex jobs. `scripts/run-gamecult-orchestrator.ps1` is the live wall-clock pulse owner; the old repo Face scheduled task installer remains only a wrapper/backcompat surface.
|
||||
- Interaction-memory logic is now split into event/tone analysis and profile-synthesis modules instead of one slab trying to be both the ear and the therapist.
|
||||
|
|
@ -253,5 +254,5 @@ rejected_paths:
|
|||
reason: sleep/distillation that collapses concrete claims into generic abstraction creates memory nonsense and then forces later cleanup compensators
|
||||
|
||||
current_status:
|
||||
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, and the repo Face swarm active-stack fault has been repaired. The worker had died, and a direct restart exposed that `claimRunnableJobs()` batch-claimed the approved backlog while processing it sequentially, making many Faces appear active even though only one turn could move. The worker now claims one runnable job per poll, and heartbeat reconciliation treats `running` jobs as active while `approved` jobs are queued blockers only. Direct worker PID 36736 is running after the repair; visible active count dropped to one. Public speech is still parent-Interpreter owned, silence/code-fence transport guards remain in place, GameCult Local Orchestrator remains the pulse owner, and Bifrost work routing remains paused by default.
|
||||
summary: VoidBot's bot/worker/provider/RAG/Postgres/Qdrant/CultCache structure is live, and the repo Face swarm active-stack fault has been repaired. The worker had died, and a direct restart exposed that `claimRunnableJobs()` batch-claimed the approved backlog while processing it sequentially, making many Faces appear active even though only one turn could move. The worker now claims one runnable job per poll, and heartbeat reconciliation treats `running` jobs as active while `approved` jobs are queued blockers only. Direct worker PID 26720 is running after the self-transcript tool pass; visible active count dropped to one. Public speech is still parent-Interpreter owned, silence/code-fence transport guards remain in place, GameCult Local Orchestrator remains the pulse owner, and Bifrost work routing remains paused by default.
|
||||
next_action: Watch the current single running repo Face job and the remaining approved backlog drain. If chat motion still looks absent, distinguish ordinary Interpreter `say=0` decisions and idle cooling from transport failure; investigate the separate CultCache/CultMesh decode error (`Extra ... byte(s) found at buffer[1]`) if it recurs on a `say=1` job or before a Discord bridge artifact is written.
|
||||
|
|
|
|||
Loading…
Reference in a new issue