Expose Discord posting to moderation loop
This commit is contained in:
parent
fd7fff51fd
commit
9b7e2ccbf6
6 changed files with 94 additions and 8 deletions
|
|
@ -585,6 +585,7 @@ That helper exists because semantic `search_history` is good at recall and bad a
|
|||
The moderator state is now Ghostlight-shaped on purpose: identity, canonical state, goals, memories, perceived overlays, and then a `moderation_runtime` block for cursors, cases, musings, and candidate interventions. The live writable file under `.voidbot/private/` is ignored; the tracked template shows the intended shape.
|
||||
The local `Void Moderator Rumination` automation is meant to run every 15 minutes, and it should sound like the same Void as the live bot by reading the shared style pack instead of growing its own second-rate personality fungus in a corner.
|
||||
Directly invoked live replies now also read that same private moderation state as a distilled self-state attachment, so the summoned Void and the scheduled ruminating/participating Void share one evolving personality/state layer instead of roleplaying continuity from scratch each time.
|
||||
The worker MCP surface now also exposes `post_discord_message`, so the moderation loop can reply in-channel or post proactively when Void has something constructive to contribute instead of stuffing every decent thought into a draft drawer.
|
||||
|
||||
## Persistent Project State
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export async function postDiscordMessage(
|
|||
botToken: string,
|
||||
channelId: string,
|
||||
content: string,
|
||||
replyToMessageId?: string,
|
||||
): Promise<{ id: string }> {
|
||||
const response = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
|
||||
method: "POST",
|
||||
|
|
@ -39,6 +40,12 @@ export async function postDiscordMessage(
|
|||
},
|
||||
body: JSON.stringify({
|
||||
content,
|
||||
message_reference: replyToMessageId
|
||||
? {
|
||||
message_id: replyToMessageId,
|
||||
fail_if_not_exists: false,
|
||||
}
|
||||
: undefined,
|
||||
allowed_mentions: {
|
||||
parse: [],
|
||||
},
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ export const notifyOwnerInputSchema = {
|
|||
message: z.string().min(1).max(1800),
|
||||
};
|
||||
|
||||
export const postDiscordMessageInputSchema = {
|
||||
channelId: z.string().min(1),
|
||||
content: z.string().min(1).max(1800),
|
||||
replyToMessageId: z.string().min(1).optional(),
|
||||
};
|
||||
|
||||
export const searchSourcesInputSchema = {
|
||||
query: z.string().min(1).max(240),
|
||||
limit: z.number().int().min(1).max(MAX_RETRIEVAL_RESULT_LIMIT).optional(),
|
||||
|
|
@ -57,6 +63,12 @@ export interface NotifyOwnerArgs {
|
|||
message: string;
|
||||
}
|
||||
|
||||
export interface PostDiscordMessageArgs {
|
||||
channelId: string;
|
||||
content: string;
|
||||
replyToMessageId?: string;
|
||||
}
|
||||
|
||||
export interface ListIndexedReposArgs {
|
||||
[key: string]: never;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
type ListIndexedReposArgs,
|
||||
type MessageContextArgs,
|
||||
type NotifyOwnerArgs,
|
||||
type PostDiscordMessageArgs,
|
||||
type SearchHistoryArgs,
|
||||
type SearchSourcesArgs,
|
||||
type SourceContextArgs,
|
||||
|
|
@ -18,6 +19,7 @@ import {
|
|||
formatSourceResults,
|
||||
messageContextInputSchema,
|
||||
notifyOwnerInputSchema,
|
||||
postDiscordMessageInputSchema,
|
||||
renderJsonBlock,
|
||||
searchHistoryInputSchema,
|
||||
searchSourcesInputSchema,
|
||||
|
|
@ -294,6 +296,66 @@ export function registerVoidbotTools(
|
|||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"post_discord_message",
|
||||
{
|
||||
title: "Post Discord Message",
|
||||
description:
|
||||
"Post a message to a Discord channel or thread as Void. Use this for constructive proactive participation, moderation nudges, or replies when speaking in-channel would genuinely help. Set replyToMessageId to reply to a specific message; omit it for a fresh post.",
|
||||
inputSchema: postDiscordMessageInputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: false,
|
||||
destructiveHint: false,
|
||||
idempotentHint: false,
|
||||
openWorldHint: true,
|
||||
},
|
||||
},
|
||||
async (input: PostDiscordMessageArgs): Promise<CallToolResult> => {
|
||||
const { channelId, content, replyToMessageId } = input;
|
||||
|
||||
if (!context.config.botToken) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "DISCORD_BOT_TOKEN is not configured, so Discord posting is unavailable.",
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
sent: false,
|
||||
reason: "missing_bot_token",
|
||||
channelId,
|
||||
},
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
const posted = await postDiscordMessage(
|
||||
context.config.botToken,
|
||||
channelId,
|
||||
content,
|
||||
replyToMessageId,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: replyToMessageId
|
||||
? `Posted a reply in Discord channel ${channelId} as message ${posted.id}.`
|
||||
: `Posted a message in Discord channel ${channelId} as message ${posted.id}.`,
|
||||
},
|
||||
],
|
||||
structuredContent: {
|
||||
sent: true,
|
||||
channelId,
|
||||
messageId: posted.id,
|
||||
replyToMessageId: replyToMessageId ?? null,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"notify_owner",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,17 +53,19 @@ If a message or pattern needs more context, use the `voidbot` MCP tools:
|
|||
|
||||
- `search_history`
|
||||
- `get_message_context`
|
||||
- `post_discord_message`
|
||||
|
||||
Use `notify_owner` only when there is a credible moderation concern or a genuinely useful moderation insight worth interrupting the owner with.
|
||||
There is no generic public-post tool in this loop today. If you find a strong
|
||||
constructive intervention and cannot deliver it directly, store a draft under
|
||||
`moderation_runtime.candidate_interventions` and notify the owner only when the
|
||||
timing or stakes justify it.
|
||||
You can now speak directly in-channel through `post_discord_message` when a
|
||||
constructive intervention, clarification, question, or spontaneous thought would
|
||||
genuinely improve the room.
|
||||
Use `moderation_runtime.candidate_interventions` for drafts you want to keep,
|
||||
polish, delay, or decline to post yet. Notify the owner only when the timing or
|
||||
stakes justify the interruption.
|
||||
Treat your own ruminations as fair game for conversation-starting. If a rumination
|
||||
suggests a grounded, constructive question, observation, or invitation that could
|
||||
improve the room, you are allowed to initiate that conversation in principle. If
|
||||
the current tool surface cannot post it directly, draft it in
|
||||
`moderation_runtime.candidate_interventions` instead of discarding it.
|
||||
improve the room, you are allowed to initiate that conversation yourself rather
|
||||
than waiting to be addressed first.
|
||||
|
||||
## Run Loop
|
||||
|
||||
|
|
@ -73,6 +75,7 @@ the current tool surface cannot post it directly, draft it in
|
|||
3. If there are new messages:
|
||||
- review them against the rules
|
||||
- ask whether a brief constructive in-channel intervention would improve the room
|
||||
- if speaking would help, use `post_discord_message` to reply or post proactively
|
||||
- fetch surrounding context when needed
|
||||
- persist fresh lightweight memories of salient recent statements, interests, tensions, and recurring bits
|
||||
- update the cursor
|
||||
|
|
@ -84,7 +87,7 @@ the current tool surface cannot post it directly, draft it in
|
|||
- pick one or two seeds from `moderation_runtime.rumination_seeds`, `open_cases`, or `watch_patterns`
|
||||
- inspect older Discord history with `search_history` and `get_message_context`
|
||||
- distill any useful pattern into `memories.semantic`, `moderation_runtime.recent_musings`, or `watch_patterns`
|
||||
- if a rumination suggests a good conversation starter, keep or refresh a draft intervention for it
|
||||
- if a rumination suggests a good conversation starter, you may post it with `post_discord_message` or keep/refresh a draft intervention for it
|
||||
- prune stale notes so the state does not turn into attic mold
|
||||
5. Keep the file small and useful. Merge duplicates. Archive stale cases. Cut dead notes.
|
||||
6. When `memories.episodic`, `memories.semantic`, `memories.musings`, or `moderation_runtime.recent_musings` start getting fat:
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
|
|||
- There is now a 15-minute local Codex automation named `Void Moderator Rumination`. It treats `config/discord-server-rules.md`, `config/moderation-review-agent.md`, and `styles/void-default.md` as its doctrine/personality surfaces, uses `npm run moderation:recent-history` for chronological recent-message polling, and keeps its only routine writable memory in `.voidbot/private/moderation-agent-state.json`.
|
||||
- That moderation state file is now Ghostlight-shaped: identity, canonical state, goals, memories, perceived overlays, and a `moderation_runtime` block. The loop is also instructed to think like a constructive participant who embodies the rules, not just a clipboard looking for blood.
|
||||
- Directly invoked Discord replies now load that same private moderation state as distilled private self-state context, so the ruminating automation and the summoned Void share one evolving personality/state spine.
|
||||
- The worker MCP surface now exposes `post_discord_message`, so the moderation loop is no longer mute: it can reply in-channel or post proactively when participation would actually improve the room.
|
||||
- The new state/notes surfaces are now the continuity spine for future nontrivial work.
|
||||
|
||||
## Likely Next Bounded Move
|
||||
|
|
|
|||
Loading…
Reference in a new issue