Relax source grounding and pin owner Codex model

This commit is contained in:
Meta Vens 2026-05-02 19:04:09 +01:00
commit 2c32b8df1f
16 changed files with 37 additions and 84 deletions

View file

@ -41,6 +41,7 @@ ENABLED_PROVIDERS=owner_codex
OWNER_CODEX_MODE=local_exec_owner_only
CODEX_EXECUTABLE=codex
CODEX_EXEC_ARGS=
CODEX_MODEL=gpt-5.4
CODEX_MODEL_REASONING_EFFORT=low
CODEX_EXEC_TIMEOUT_MS=120000
STYLE_PACK_PATH=styles/void-default.md

View file

@ -213,7 +213,7 @@ npm run dev:worker
If `DISCORD_APPLICATION_ID` and `DISCORD_GUILD_ID` are set, the bot registers slash commands into that guild on startup.
For `owner_codex`, the worker also needs a working local Codex CLI. If plain `codex` is not invokable from Node on your machine, point `CODEX_EXECUTABLE` and `CODEX_EXEC_ARGS` at the actual install.
For `owner_codex`, the worker also needs a working local Codex CLI. If plain `codex` is not invokable from Node on your machine, point `CODEX_EXECUTABLE` and `CODEX_EXEC_ARGS` at the actual install. `CODEX_MODEL` pins the owner Discord lane to a specific model; the default is `gpt-5.4` so older local Codex installs do not faceplant on `gpt-5.5`.
## Example Questions
@ -262,7 +262,7 @@ The `local_llm` provider uses Ollama chat completion plus a bounded host-managed
Unlike the owner Codex lane, this is not MCP. It is a tighter host-controlled loop and deliberately excludes side-effecting tools.
Project, repo, and lore questions are now source-grounded by policy: if the prompt matches indexed repo/project cues, Void must touch the source-side lookup tools before answering confidently.
Project, repo, and lore questions should lean on the source-side lookup tools when the answer depends on indexed repo or lore facts. The providers now treat source-grounding hints as advisory instead of letting a dumb lexical hall monitor veto the whole reply before the model can think.
## Discord, Repo, And Lore Indexing

View file

@ -288,7 +288,7 @@ export function inferSourceGroundingHint(
}
return {
required: reasons.size > 0,
required: false,
reasons: [...reasons],
matchedRepoNames: [...matchedRepoNames].sort(),
};

View file

@ -149,6 +149,7 @@ export async function startBot(): Promise<void> {
mode: config.ownerCodexMode,
executable: config.codexExecutable,
executableArgs: config.codexExecArgs,
model: config.codexModel,
reasoningEffort: config.codexModelReasoningEffort,
timeoutMs: config.codexExecTimeoutMs,
workingDirectory: process.cwd(),

View file

@ -136,6 +136,7 @@ async function buildProviderRegistry(systemMessagesCatalog: SystemMessageCatalog
mode: config.ownerCodexMode,
executable: config.codexExecutable,
executableArgs: config.codexExecArgs,
model: config.codexModel,
reasoningEffort: config.codexModelReasoningEffort,
timeoutMs: config.codexExecTimeoutMs,
workingDirectory: process.cwd(),

View file

@ -32,6 +32,7 @@ Git history and smoke artifacts carry routine proof. `state/evidence.jsonl` shou
- The stack has a dedicated logon startup task path now, so a reboot does not have to mean manual necromancy for bot and worker.
- The operations watchdog now supports ignored local extension checks, so adjacent-service fire signals can ride the same owner DM alert path without hardcoding private project gossip into the public repo.
- Source repo discovery now reconciles automatically during `npm run stack:start` and through the watchdog path, so new repos under `SOURCE_REPO_ROOT` stop waiting for manual hook folklore before they get indexed.
- Source-grounding hints are advisory now; the owner and local lanes can decide when repo or lore tool use is actually warranted instead of obeying a lexical front-door veto.
- The new state/notes surfaces are now the continuity spine for future nontrivial work.
## Likely Next Bounded Move

View file

@ -86,6 +86,7 @@ const envSchema = z.object({
OWNER_CODEX_MODE: z.string().min(1).default("local_exec_owner_only"),
CODEX_EXECUTABLE: z.string().min(1).default("codex"),
CODEX_EXEC_ARGS: z.string().default(""),
CODEX_MODEL: z.string().min(1).default("gpt-5.4"),
CODEX_MODEL_REASONING_EFFORT: z.enum(["low", "medium", "high", "xhigh"]).default("low"),
CODEX_EXEC_TIMEOUT_MS: z.coerce.number().int().positive().default(120000),
STYLE_PACK_PATH: z.string().min(1).default("styles/void-default.md"),
@ -116,6 +117,7 @@ export interface AppConfig {
ownerCodexMode: OwnerCodexMode;
codexExecutable: string;
codexExecArgs: string[];
codexModel: string;
codexModelReasoningEffort: "low" | "medium" | "high" | "xhigh";
codexExecTimeoutMs: number;
stylePackPath: string;
@ -285,6 +287,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
ownerCodexMode: parseOwnerCodexMode(parsed.OWNER_CODEX_MODE),
codexExecutable: parsed.CODEX_EXECUTABLE,
codexExecArgs: parseList(parsed.CODEX_EXEC_ARGS),
codexModel: parsed.CODEX_MODEL,
codexModelReasoningEffort: parsed.CODEX_MODEL_REASONING_EFFORT,
codexExecTimeoutMs: parsed.CODEX_EXEC_TIMEOUT_MS,
stylePackPath: resolve(parsed.STYLE_PACK_PATH),

View file

@ -10,9 +10,7 @@ import {
import {
buildArtifacts,
buildPrompt,
buildSourceGroundingReminder,
buildSystemPrompt,
didUseSourceGrounding,
} from "./local-llm-render";
import {
executeToolCall,
@ -20,7 +18,6 @@ import {
normalizeAssistantMessage,
} from "./local-llm-tools";
import {
MAX_SOURCE_GROUNDING_RETRIES,
MAX_TOOL_TURNS,
type LocalLlmProviderOptions,
type OllamaChatMessage,
@ -96,8 +93,6 @@ export class LocalLlmProvider implements ProviderAdapter {
const tools = this.options.toolbox ? buildToolDefinitions() : undefined;
const toolTrace: ToolTraceRecord[] = [];
const thinkingTrace: string[] = [];
const sourceGroundingRequired = Boolean(request.contextBundle.sourceGrounding?.required);
let sourceGroundingReminderCount = 0;
try {
let finalPayload: OllamaChatResponse | undefined;
@ -147,25 +142,6 @@ export class LocalLlmProvider implements ProviderAdapter {
if (toolCalls.length === 0) {
const candidateOutput = normalizeModelText(assistantMessage.content);
if (
sourceGroundingRequired &&
!didUseSourceGrounding(toolTrace) &&
this.options.toolbox
) {
if (sourceGroundingReminderCount < MAX_SOURCE_GROUNDING_RETRIES) {
sourceGroundingReminderCount += 1;
messages.push({
role: "user",
content: buildSourceGroundingReminder(request.contextBundle),
});
continue;
}
finalOutputText =
"This request needs actual source grounding before I can answer it cleanly. Ask again after I pull from the indexed repos, or use the deeper Codex lane.";
break;
}
finalOutputText = candidateOutput;
break;
}

View file

@ -79,18 +79,6 @@ export function buildPrompt(context: ContextBundle): string {
].join("\n");
}
export function buildSourceGroundingReminder(context: ContextBundle): string {
return `${renderSourceGroundingInstructions(context, true)} Answer again only after using the source-side tools.`;
}
export function didUseSourceGrounding(toolTrace: ToolTraceRecord[]): boolean {
return toolTrace.some((trace) =>
trace.tool === "list_indexed_repos" ||
trace.tool === "search_sources" ||
trace.tool === "get_source_context",
);
}
export function buildArtifacts(input: LocalLlmArtifactsInput): ProviderArtifact[] {
return buildBaseArtifacts(input);
}
@ -140,16 +128,12 @@ function renderSourceGroundingInstructions(
context: ContextBundle,
reminder: boolean,
): string {
if (!context.sourceGrounding?.required) {
return "Source-side grounding is optional here; use it when it clearly helps.";
}
const matchedRepos =
context.sourceGrounding.matchedRepoNames.length > 0
context.sourceGrounding?.matchedRepoNames && context.sourceGrounding.matchedRepoNames.length > 0
? ` Matched repos/projects: ${context.sourceGrounding.matchedRepoNames.join(", ")}.`
: "";
const reasons =
context.sourceGrounding.reasons.length > 0
context.sourceGrounding?.reasons && context.sourceGrounding.reasons.length > 0
? ` Reasons: ${context.sourceGrounding.reasons.join(", ")}.`
: "";
const retry =
@ -157,5 +141,13 @@ function renderSourceGroundingInstructions(
? " The previous answer attempt was discarded because it did not touch the source-side tools."
: "";
return `This prompt requires source-grounded evidence before you answer. Use list_indexed_repos, search_sources, or get_source_context first.${matchedRepos}${reasons}${retry}`;
if (
!context.sourceGrounding ||
(context.sourceGrounding.matchedRepoNames.length === 0 &&
context.sourceGrounding.reasons.length === 0)
) {
return "Source-side grounding is optional here; use it when it clearly helps.";
}
return `This prompt may benefit from source-side grounding. Use list_indexed_repos, search_sources, or get_source_context if repo, lore, or source evidence would sharpen the answer.${matchedRepos}${reasons}${retry}`;
}

View file

@ -5,7 +5,6 @@ import {
export type OllamaThinkMode = boolean | "low" | "medium" | "high";
export const MAX_TOOL_TURNS = 4;
export const MAX_SOURCE_GROUNDING_RETRIES = 1;
export const MAX_TOOL_RESULTS = MAX_RETRIEVAL_RESULT_LIMIT;
export const MAX_CONTEXT_WINDOW = 20;

View file

@ -11,10 +11,8 @@ import type {
import {
buildDefaultHandoffNotice,
buildRequestPayload,
didUseSourceGrounding,
type HistoryLookupTool,
MAX_HISTORY_TOOL_CALLS,
MAX_SOURCE_GROUNDING_RETRIES,
type OwnerCodexProviderOptions,
type ToolCallRecord,
} from "./owner-codex-shared";
@ -176,25 +174,18 @@ export class OwnerCodexProvider implements ProviderAdapter {
const artifacts: ProviderArtifact[] = [...baseArtifacts];
const turnResults = [];
let result;
let sourceGroundingReminderCount = 0;
const sourceGroundingRequired = Boolean(
request.contextBundle.sourceGrounding?.required,
);
for (
let turn = 0;
turn <= MAX_HISTORY_TOOL_CALLS + MAX_SOURCE_GROUNDING_RETRIES;
turn += 1
) {
for (let turn = 0; turn <= MAX_HISTORY_TOOL_CALLS; turn += 1) {
const codexPrompt = buildDiscordReplyPrompt(
request.contextBundle,
toolCalls,
sourceGroundingReminderCount > 0,
false,
);
result = await runCodexExec({
executable: this.options.executable,
executableArgs: this.options.executableArgs,
model: this.options.model,
reasoningEffort: this.options.reasoningEffort,
timeoutMs: this.options.timeoutMs,
workingDirectory: this.options.workingDirectory,
@ -246,16 +237,6 @@ export class OwnerCodexProvider implements ProviderAdapter {
}
if (!result.toolRequest) {
if (sourceGroundingRequired && !didUseSourceGrounding(result.traceEvents)) {
if (sourceGroundingReminderCount < MAX_SOURCE_GROUNDING_RETRIES) {
sourceGroundingReminderCount += 1;
continue;
}
result.handoffReason =
"This question needed source-grounded evidence, but the reply lane never used the source tools.";
}
break;
}

View file

@ -333,7 +333,11 @@ function renderSourceGroundingInstructions(
context: ContextBundle,
reminder: boolean,
): string {
if (!context.sourceGrounding?.required) {
if (
!context.sourceGrounding ||
(context.sourceGrounding.matchedRepoNames.length === 0 &&
context.sourceGrounding.reasons.length === 0)
) {
return "- Source-side grounding is optional here; use it when it clearly helps.";
}
@ -349,7 +353,7 @@ function renderSourceGroundingInstructions(
? " The previous answer attempt was discarded because it did not touch the source-side tools."
: "";
return `- This prompt requires source-grounded evidence before you answer. Use list_indexed_repos, search_sources, or get_source_context first.${matchedRepos}${reasons}${retry}`;
return `- This prompt may benefit from source-side grounding. Use list_indexed_repos, search_sources, or get_source_context if repo, lore, or source evidence would sharpen the answer.${matchedRepos}${reasons}${retry}`;
}
function renderAgentMessageTrace(events: CodexTraceEvent[]): string {

View file

@ -42,6 +42,7 @@ const MAX_HISTORY_LIMIT = MAX_RETRIEVAL_RESULT_LIMIT;
export async function runCodexExec(input: {
executable: string;
executableArgs: string[];
model: string;
reasoningEffort: "low" | "medium" | "high" | "xhigh";
timeoutMs: number;
workingDirectory: string;
@ -54,6 +55,8 @@ export async function runCodexExec(input: {
const args = [
...input.executableArgs,
"exec",
"-m",
input.model,
"-c",
'approval_policy="never"',
"-c",

View file

@ -12,7 +12,6 @@ export const HANDOFF_SENTINEL = "VOIDBOT_HANDOFF_REQUIRED:";
export const OWNER_NOTIFY_SENTINEL = "VOIDBOT_OWNER_NOTIFY:";
export const TOOL_REQUEST_SENTINEL = "VOIDBOT_TOOL_REQUEST:";
export const MAX_HISTORY_TOOL_CALLS = 4;
export const MAX_SOURCE_GROUNDING_RETRIES = 1;
export const MAX_NOTIFICATION_MESSAGE_LENGTH = 400;
export interface CodexUsageSnapshot {
@ -35,6 +34,7 @@ export interface OwnerCodexProviderOptions {
mode: OwnerCodexMode;
executable: string;
executableArgs: string[];
model: string;
reasoningEffort: "low" | "medium" | "high" | "xhigh";
timeoutMs: number;
workingDirectory: string;
@ -133,16 +133,6 @@ export function buildDefaultHandoffNotice(jobId: string): string {
return `This request needs the fuller local Codex flow. Check \`.voidbot/artifacts/${jobId}/handoff.md\` and \`.voidbot/artifacts/${jobId}/debug-trace.md\` in the local workspace.`;
}
export function didUseSourceGrounding(events: CodexTraceEvent[]): boolean {
return events.some(
(event) =>
event.kind === "mcp_tool_completed" &&
(event.tool === "list_indexed_repos" ||
event.tool === "search_sources" ||
event.tool === "get_source_context"),
);
}
export function extractLatestUsage(
events: CodexTraceEvent[],
): CodexUsageSnapshot | undefined {

View file

@ -19,3 +19,4 @@
{"ts":"2026-05-02T14:05:54Z","type":"implementation","status":"ok","note":"Scheduled-task helpers must anchor repo-relative scripts, dotenv loading, and path resolution to the VoidBot root; relying on cwd keeps sending System32 in to eat them."}
{"ts":"2026-05-02T17:34:02Z","type":"implementation","status":"ok","note":"Layered private system message catalogs over the tracked base, deferred slow slash replies before provider execution, and chunked Discord output so missing keys and overlong local_llm replies stop failing live interactions."}
{"ts":"2026-05-02T17:39:46Z","type":"implementation","status":"ok","note":"Source-grounding alias matching now ignores overly short repo components so social prompts mentioning Void stop tripping fake repo-evidence requirements, and direct messages intentionally count as prompts without requiring a mention."}
{"ts":"2026-05-02T18:28:04Z","type":"implementation","status":"ok","note":"Pinned the owner Codex lane to a configurable CODEX_MODEL defaulting to gpt-5.4 because the local CLI still faceplants on gpt-5.5, and relaxed source-grounding from a lexical hard gate into an advisory hint so the model can decide when repo/lore tool use is actually needed."}

View file

@ -30,7 +30,7 @@ invariants:
- Scratch is temporary and disposable.
- Evidence is durable and distilled; routine activity proof belongs in git history, commits, smoke artifacts, or targeted logs.
- Jobs, audit events, interaction memory, and rate-limit state live in Postgres; vectors live in Qdrant; archives, artifacts, logs, and backups remain under `.voidbot/`.
- Source, repo, and lore answers must be grounded through source-side retrieval instead of vibes.
- Source, repo, and lore answers should use source-side retrieval when the reply depends on indexed facts; the hinting is advisory, not a lexical veto.
- When GameCult history, repo, or lore retrieval can answer the question, use the VoidBot MCP surfaces before raw repo spelunking.
data_flow:
@ -75,7 +75,7 @@ landed_baseline:
- Local and offsite backups, watchdogs, and the operations dashboard are live.
- The stack now has a dedicated logon startup task path so bot and worker can come back after a reboot without waiting for manual resurrection.
- Interaction memory and behavioral profiling are live.
- Source-grounding enforcement and tool-driven retrieval are live.
- Tool-driven retrieval is live, and source-grounding hints now bias the reply lanes instead of hard-gating them with a lexical pre-classifier.
- The operations watchdog can now load ignored local extension checks, so adjacent-service fire signals can piggyback on the owner DM path without turning the public repo into cross-project gossip.
- The owner Codex lane is now split into orchestration, runtime/parsing, shared helpers, and rendering modules instead of one swollen provider file.
- The local Ollama lane is now split into orchestration, prompt/render helpers, tool-loop helpers, and shared types/constants instead of one swollen provider file.