mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix: pass system prompt to codex cli
This commit is contained in:
@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Android/pairing: clear stale setup-code auth on new QR scans, bootstrap operator and node sessions from fresh pairing, prefer stored device tokens after bootstrap handoff, and pause pairing auto-retry while the app is backgrounded so scan-once Android pairing recovers reliably again. (#63199) Thanks @obviyus.
|
||||
- Auto-reply/NO_REPLY: strip glued leading `NO_REPLY` tokens before reply normalization and ACP-visible streaming so silent sentinel text no longer leaks into user-visible replies while preserving substantive `NO_REPLY ...` text. Thanks @frankekn.
|
||||
- Gateway/sessions: clear auto-fallback-pinned model overrides on `/reset` and `/new` while still preserving explicit user model selections, including legacy sessions created before override-source tracking existed. (#63155) Thanks @frankekn.
|
||||
- Codex CLI: pass OpenClaw's system prompt through Codex's `model_instructions_file` config override so fresh Codex CLI sessions receive the same prompt guidance as Claude CLI sessions.
|
||||
|
||||
## 2026.4.8
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
6092701439f9f56624f508eb2b240cb48375264c2667a99cb7e7823cb0ef18d1 config-baseline.json
|
||||
065f474b340fc22b19358cb298131037cbb2a3411ef0b6f765072bbaafedf751 config-baseline.core.json
|
||||
0a75b57f5dbb0bb1488eacb47111ee22ff42dd3747bfe07bb69c9445d5e55c3e config-baseline.json
|
||||
ff15bb8b4231fc80174249ae89bcb61439d7adda5ee6be95e4d304680253a59f config-baseline.core.json
|
||||
7f42b22b46c487d64aaac46001ba9d9096cf7bf0b1c263a54d39946303ff5018 config-baseline.channel.json
|
||||
483d4f3c1d516719870ad6f2aba6779b9950f85471ee77b9994a077a7574a892 config-baseline.plugin.json
|
||||
|
||||
@@ -124,6 +124,9 @@ The provider id becomes the left side of your model ref:
|
||||
sessionMode: "existing",
|
||||
sessionIdFields: ["session_id", "conversation_id"],
|
||||
systemPromptArg: "--system",
|
||||
// Codex-style CLIs can point at a prompt file instead:
|
||||
// systemPromptFileConfigArg: "-c",
|
||||
// systemPromptFileConfigKey: "model_instructions_file",
|
||||
systemPromptWhen: "first",
|
||||
imageArg: "--image",
|
||||
imageMode: "repeat",
|
||||
@@ -150,6 +153,12 @@ told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats
|
||||
a new policy.
|
||||
</Note>
|
||||
|
||||
The bundled OpenAI `codex-cli` backend passes OpenClaw's system prompt through
|
||||
Codex's `model_instructions_file` config override (`-c
|
||||
model_instructions_file="..."`). Codex does not expose a Claude-style
|
||||
`--append-system-prompt` flag, so OpenClaw writes the assembled prompt to a
|
||||
temporary file for each fresh Codex CLI session.
|
||||
|
||||
## Sessions
|
||||
|
||||
- If the CLI supports sessions, set `sessionArg` (e.g. `--session-id`) or
|
||||
|
||||
@@ -38,6 +38,9 @@ export function buildOpenAICodexCliBackend(): CliBackendPlugin {
|
||||
modelArg: "--model",
|
||||
sessionIdFields: ["thread_id"],
|
||||
sessionMode: "existing",
|
||||
systemPromptFileConfigArg: "-c",
|
||||
systemPromptFileConfigKey: "model_instructions_file",
|
||||
systemPromptWhen: "first",
|
||||
imageArg: "--image",
|
||||
imageMode: "repeat",
|
||||
reliability: {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { CliBackendConfig } from "../config/types.js";
|
||||
import type { CliBundleMcpMode } from "../plugins/types.js";
|
||||
|
||||
let createEmptyPluginRegistry: typeof import("../plugins/registry.js").createEmptyPluginRegistry;
|
||||
let resetPluginRuntimeStateForTest: typeof import("../plugins/runtime.js").resetPluginRuntimeStateForTest;
|
||||
let setActivePluginRegistry: typeof import("../plugins/runtime.js").setActivePluginRegistry;
|
||||
let normalizeClaudeBackendConfig: typeof import("./cli-backends.js").normalizeClaudeBackendConfig;
|
||||
let resolveCliBackendConfig: typeof import("./cli-backends.js").resolveCliBackendConfig;
|
||||
@@ -96,11 +97,16 @@ beforeAll(async () => {
|
||||
vi.doUnmock("../plugins/setup-registry.js");
|
||||
vi.doUnmock("../plugins/cli-backends.runtime.js");
|
||||
({ createEmptyPluginRegistry } = await import("../plugins/registry.js"));
|
||||
({ setActivePluginRegistry } = await import("../plugins/runtime.js"));
|
||||
({ resetPluginRuntimeStateForTest, setActivePluginRegistry } =
|
||||
await import("../plugins/runtime.js"));
|
||||
({ normalizeClaudeBackendConfig, resolveCliBackendConfig, resolveCliBackendLiveTest } =
|
||||
await import("./cli-backends.js"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.cliBackends = [
|
||||
@@ -177,6 +183,9 @@ beforeEach(() => {
|
||||
"--skip-git-repo-check",
|
||||
],
|
||||
resumeArgs: ["exec", "resume", "{sessionId}", "--dangerously-bypass-approvals-and-sandbox"],
|
||||
systemPromptFileConfigArg: "-c",
|
||||
systemPromptFileConfigKey: "model_instructions_file",
|
||||
systemPromptWhen: "first",
|
||||
reliability: {
|
||||
watchdog: {
|
||||
fresh: {
|
||||
@@ -657,6 +666,9 @@ describe("resolveCliBackendConfig google-gemini-cli defaults", () => {
|
||||
expect(resolved).not.toBeNull();
|
||||
expect(resolved?.bundleMcp).toBe(true);
|
||||
expect(resolved?.bundleMcpMode).toBe("codex-config-overrides");
|
||||
expect(resolved?.config.systemPromptFileConfigArg).toBe("-c");
|
||||
expect(resolved?.config.systemPromptFileConfigKey).toBe("model_instructions_file");
|
||||
expect(resolved?.config.systemPromptWhen).toBe("first");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
prepareCliPromptImagePayload,
|
||||
resolveCliRunQueueKey,
|
||||
writeCliImages,
|
||||
writeCliSystemPromptFile,
|
||||
} from "./cli-runner/helpers.js";
|
||||
import * as promptImageUtils from "./pi-embedded-runner/run/images.js";
|
||||
import type { SandboxFsBridge } from "./sandbox/fs-bridge.js";
|
||||
@@ -143,6 +144,23 @@ describe("buildCliArgs", () => {
|
||||
).toEqual(["-p", "--append-system-prompt", "Stable prefix\nDynamic suffix"]);
|
||||
});
|
||||
|
||||
it("passes Codex system prompts via a model instructions file config override", () => {
|
||||
expect(
|
||||
buildCliArgs({
|
||||
backend: {
|
||||
command: "codex",
|
||||
systemPromptFileConfigArg: "-c",
|
||||
systemPromptFileConfigKey: "model_instructions_file",
|
||||
},
|
||||
baseArgs: ["exec", "--json"],
|
||||
modelId: "gpt-5.4",
|
||||
systemPrompt: "Stable prefix",
|
||||
systemPromptFilePath: "/tmp/openclaw/system-prompt.md",
|
||||
useResume: false,
|
||||
}),
|
||||
).toEqual(["exec", "--json", "-c", 'model_instructions_file="/tmp/openclaw/system-prompt.md"']);
|
||||
});
|
||||
|
||||
it("replaces prompt placeholders before falling back to a trailing positional prompt", () => {
|
||||
expect(
|
||||
buildCliArgs({
|
||||
@@ -412,6 +430,28 @@ describe("writeCliImages", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeCliSystemPromptFile", () => {
|
||||
it("writes stripped system prompts to a private temp file", async () => {
|
||||
const written = await writeCliSystemPromptFile({
|
||||
backend: {
|
||||
command: "codex",
|
||||
systemPromptFileConfigKey: "model_instructions_file",
|
||||
},
|
||||
systemPrompt: `Stable prefix${SYSTEM_PROMPT_CACHE_BOUNDARY}Dynamic suffix`,
|
||||
});
|
||||
|
||||
try {
|
||||
expect(written.filePath).toContain("openclaw-cli-system-prompt-");
|
||||
await expect(fs.readFile(written.filePath ?? "", "utf-8")).resolves.toBe(
|
||||
"Stable prefix\nDynamic suffix",
|
||||
);
|
||||
} finally {
|
||||
await written.cleanup();
|
||||
}
|
||||
await expect(fs.access(written.filePath ?? "")).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveCliRunQueueKey", () => {
|
||||
it("scopes Claude CLI serialization to the workspace for fresh runs", () => {
|
||||
expect(
|
||||
|
||||
@@ -51,6 +51,9 @@ function buildPreparedCliRunContext(params: {
|
||||
input: "arg" as const,
|
||||
modelArg: "--model",
|
||||
sessionMode: "existing" as const,
|
||||
systemPromptFileConfigArg: "-c",
|
||||
systemPromptFileConfigKey: "model_instructions_file",
|
||||
systemPromptWhen: "first" as const,
|
||||
serialize: true,
|
||||
};
|
||||
const backend = { ...baseBackend, ...params.backend };
|
||||
@@ -221,6 +224,39 @@ describe("runCliAgent spawn path", () => {
|
||||
expect(input.scopeKey).toContain("thread-123");
|
||||
});
|
||||
|
||||
it("passes Codex system prompts through model_instructions_file", async () => {
|
||||
let promptFileText = "";
|
||||
supervisorSpawnMock.mockImplementationOnce(async (...args: unknown[]) => {
|
||||
const input = (args[0] ?? {}) as { argv?: string[] };
|
||||
const configArgIndex = input.argv?.indexOf("-c") ?? -1;
|
||||
expect(configArgIndex).toBeGreaterThanOrEqual(0);
|
||||
const configArg = input.argv?.[configArgIndex + 1] ?? "";
|
||||
const match = /^model_instructions_file="(.+)"$/.exec(configArg);
|
||||
expect(match?.[1]).toBeTruthy();
|
||||
promptFileText = await fs.readFile(match?.[1] ?? "", "utf-8");
|
||||
return createManagedRun({
|
||||
reason: "exit",
|
||||
exitCode: 0,
|
||||
exitSignal: null,
|
||||
durationMs: 50,
|
||||
stdout: "ok",
|
||||
stderr: "",
|
||||
timedOut: false,
|
||||
noOutputTimedOut: false,
|
||||
});
|
||||
});
|
||||
|
||||
await executePreparedCliRun(
|
||||
buildPreparedCliRunContext({
|
||||
provider: "codex-cli",
|
||||
model: "gpt-5.4",
|
||||
runId: "run-codex-system-prompt-file",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(promptFileText).toBe("You are a helpful assistant.");
|
||||
});
|
||||
|
||||
it("cancels the managed CLI run when the abort signal fires", async () => {
|
||||
const abortController = new AbortController();
|
||||
let resolveWait!: (value: {
|
||||
|
||||
@@ -135,6 +135,9 @@ function buildOpenAICodexCliBackendFixture(): CliBackendPlugin {
|
||||
modelArg: "--model",
|
||||
sessionIdFields: ["thread_id"],
|
||||
sessionMode: "existing",
|
||||
systemPromptFileConfigArg: "-c",
|
||||
systemPromptFileConfigKey: "model_instructions_file",
|
||||
systemPromptWhen: "first",
|
||||
imageArg: "--image",
|
||||
imageMode: "repeat",
|
||||
reliability: {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { serializeTomlInlineValue } from "./toml-inline.js";
|
||||
|
||||
type PreparedCliBundleMcpConfig = {
|
||||
backend: CliBackendConfig;
|
||||
@@ -204,35 +205,6 @@ function normalizeGeminiServerConfig(
|
||||
return next;
|
||||
}
|
||||
|
||||
function escapeTomlString(value: string): string {
|
||||
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
}
|
||||
|
||||
function formatTomlKey(key: string): string {
|
||||
return /^[A-Za-z0-9_-]+$/.test(key) ? key : `"${escapeTomlString(key)}"`;
|
||||
}
|
||||
|
||||
function serializeTomlInlineValue(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
return `"${escapeTomlString(value)}"`;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((entry) => serializeTomlInlineValue(entry)).join(", ")}]`;
|
||||
}
|
||||
if (isRecord(value)) {
|
||||
return `{ ${Object.entries(value)
|
||||
.map(([key, entry]) => `${formatTomlKey(key)} = ${serializeTomlInlineValue(entry)}`)
|
||||
.join(", ")} }`;
|
||||
}
|
||||
throw new Error(`Unsupported TOML value for Codex MCP config: ${String(value)}`);
|
||||
}
|
||||
|
||||
function injectCodexMcpConfigArgs(args: string[] | undefined, config: BundleMcpConfig): string[] {
|
||||
const overrides = serializeTomlInlineValue(
|
||||
Object.fromEntries(
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
resolvePromptInput,
|
||||
resolveSessionIdToSend,
|
||||
resolveSystemPromptUsage,
|
||||
writeCliSystemPromptFile,
|
||||
} from "./helpers.js";
|
||||
import {
|
||||
cliBackendLog,
|
||||
@@ -113,6 +114,13 @@ export async function executePreparedCliRun(
|
||||
isNewSession: isNew,
|
||||
systemPrompt: context.systemPrompt,
|
||||
});
|
||||
const systemPromptFile =
|
||||
!useResume && systemPromptArg
|
||||
? await writeCliSystemPromptFile({
|
||||
backend,
|
||||
systemPrompt: systemPromptArg,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
let prompt = prependBootstrapPromptWarning(params.prompt, context.bootstrapPromptWarningLines, {
|
||||
preserveExactPrompt: context.heartbeatPrompt,
|
||||
@@ -144,6 +152,7 @@ export async function executePreparedCliRun(
|
||||
modelId: context.normalizedModel,
|
||||
sessionId: resolvedSessionId,
|
||||
systemPrompt: systemPromptArg,
|
||||
systemPromptFilePath: systemPromptFile?.filePath,
|
||||
imagePaths,
|
||||
promptArg: argsPrompt,
|
||||
useResume,
|
||||
@@ -350,6 +359,9 @@ export async function executePreparedCliRun(
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
if (systemPromptFile) {
|
||||
await systemPromptFile.cleanup();
|
||||
}
|
||||
if (cleanupImages) {
|
||||
await cleanupImages();
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { stripSystemPromptCacheBoundary } from "../system-prompt-cache-boundary.
|
||||
import { buildSystemPromptParams } from "../system-prompt-params.js";
|
||||
import { buildAgentSystemPrompt } from "../system-prompt.js";
|
||||
import { sanitizeImageBlocks } from "../tool-images.js";
|
||||
import { formatTomlConfigOverride } from "./toml-inline.js";
|
||||
export { buildCliSupervisorScopeKey, resolveCliNoOutputTimeoutMs } from "./reliability.js";
|
||||
|
||||
const CLI_RUN_QUEUE = new KeyedAsyncQueue();
|
||||
@@ -153,7 +154,10 @@ export function resolveSystemPromptUsage(params: {
|
||||
if (when === "first" && !params.isNewSession) {
|
||||
return null;
|
||||
}
|
||||
if (!params.backend.systemPromptArg?.trim()) {
|
||||
if (
|
||||
!params.backend.systemPromptArg?.trim() &&
|
||||
!params.backend.systemPromptFileConfigKey?.trim()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return systemPrompt;
|
||||
@@ -280,6 +284,29 @@ export async function writeCliImages(params: {
|
||||
return { paths, cleanup };
|
||||
}
|
||||
|
||||
export async function writeCliSystemPromptFile(params: {
|
||||
backend: CliBackendConfig;
|
||||
systemPrompt: string;
|
||||
}): Promise<{ filePath?: string; cleanup: () => Promise<void> }> {
|
||||
if (!params.backend.systemPromptFileConfigKey?.trim()) {
|
||||
return { cleanup: async () => {} };
|
||||
}
|
||||
const tempDir = await fs.mkdtemp(
|
||||
path.join(resolvePreferredOpenClawTmpDir(), "openclaw-cli-system-prompt-"),
|
||||
);
|
||||
const filePath = path.join(tempDir, "system-prompt.md");
|
||||
await fs.writeFile(filePath, stripSystemPromptCacheBoundary(params.systemPrompt), {
|
||||
encoding: "utf-8",
|
||||
mode: 0o600,
|
||||
});
|
||||
return {
|
||||
filePath,
|
||||
cleanup: async () => {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareCliPromptImagePayload(params: {
|
||||
backend: CliBackendConfig;
|
||||
prompt: string;
|
||||
@@ -328,6 +355,7 @@ export function buildCliArgs(params: {
|
||||
modelId: string;
|
||||
sessionId?: string;
|
||||
systemPrompt?: string | null;
|
||||
systemPromptFilePath?: string;
|
||||
imagePaths?: string[];
|
||||
promptArg?: string;
|
||||
useResume: boolean;
|
||||
@@ -336,7 +364,20 @@ export function buildCliArgs(params: {
|
||||
if (params.backend.modelArg && params.modelId) {
|
||||
args.push(params.backend.modelArg, params.modelId);
|
||||
}
|
||||
if (!params.useResume && params.systemPrompt && params.backend.systemPromptArg) {
|
||||
if (
|
||||
!params.useResume &&
|
||||
params.systemPrompt &&
|
||||
params.systemPromptFilePath &&
|
||||
params.backend.systemPromptFileConfigKey
|
||||
) {
|
||||
args.push(
|
||||
params.backend.systemPromptFileConfigArg ?? "-c",
|
||||
formatTomlConfigOverride(
|
||||
params.backend.systemPromptFileConfigKey,
|
||||
params.systemPromptFilePath,
|
||||
),
|
||||
);
|
||||
} else if (!params.useResume && params.systemPrompt && params.backend.systemPromptArg) {
|
||||
args.push(params.backend.systemPromptArg, stripSystemPromptCacheBoundary(params.systemPrompt));
|
||||
}
|
||||
if (!params.useResume && params.sessionId) {
|
||||
|
||||
36
src/agents/cli-runner/toml-inline.ts
Normal file
36
src/agents/cli-runner/toml-inline.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
function escapeTomlString(value: string): string {
|
||||
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
||||
}
|
||||
|
||||
function formatTomlKey(key: string): string {
|
||||
return /^[A-Za-z0-9_-]+$/.test(key) ? key : `"${escapeTomlString(key)}"`;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function serializeTomlInlineValue(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
return `"${escapeTomlString(value)}"`;
|
||||
}
|
||||
if (typeof value === "number" || typeof value === "bigint") {
|
||||
return String(value);
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map((entry) => serializeTomlInlineValue(entry)).join(", ")}]`;
|
||||
}
|
||||
if (isRecord(value)) {
|
||||
return `{ ${Object.entries(value)
|
||||
.map(([key, entry]) => `${formatTomlKey(key)} = ${serializeTomlInlineValue(entry)}`)
|
||||
.join(", ")} }`;
|
||||
}
|
||||
throw new Error(`Unsupported TOML inline value: ${String(value)}`);
|
||||
}
|
||||
|
||||
export function formatTomlConfigOverride(key: string, value: unknown): string {
|
||||
return `${key}=${serializeTomlInlineValue(value)}`;
|
||||
}
|
||||
@@ -1,5 +1,20 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
async function resetProviderRuntimeState() {
|
||||
const [
|
||||
{ clearPluginManifestRegistryCache },
|
||||
{ resetProviderRuntimeHookCacheForTest },
|
||||
{ resetPluginLoaderTestStateForTest },
|
||||
] = await Promise.all([
|
||||
import("../plugins/manifest-registry.js"),
|
||||
import("../plugins/provider-runtime.js"),
|
||||
import("../plugins/loader.test-fixtures.js"),
|
||||
]);
|
||||
resetPluginLoaderTestStateForTest();
|
||||
clearPluginManifestRegistryCache();
|
||||
resetProviderRuntimeHookCacheForTest();
|
||||
}
|
||||
|
||||
let createProviderAuthResolver: typeof import("./models-config.providers.secrets.js").createProviderAuthResolver;
|
||||
|
||||
async function loadSecretsModule() {
|
||||
@@ -7,6 +22,7 @@ async function loadSecretsModule() {
|
||||
vi.doUnmock("../plugins/provider-runtime.js");
|
||||
vi.doUnmock("../secrets/provider-env-vars.js");
|
||||
vi.resetModules();
|
||||
await resetProviderRuntimeState();
|
||||
({ createProviderAuthResolver } = await import("./models-config.providers.secrets.js"));
|
||||
}
|
||||
|
||||
|
||||
@@ -3425,6 +3425,12 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
|
||||
systemPromptArg: {
|
||||
type: "string",
|
||||
},
|
||||
systemPromptFileConfigArg: {
|
||||
type: "string",
|
||||
},
|
||||
systemPromptFileConfigKey: {
|
||||
type: "string",
|
||||
},
|
||||
systemPromptMode: {
|
||||
anyOf: [
|
||||
{
|
||||
|
||||
@@ -79,6 +79,10 @@ export type CliBackendConfig = {
|
||||
sessionIdFields?: string[];
|
||||
/** Flag used to pass system prompt. */
|
||||
systemPromptArg?: string;
|
||||
/** Config override flag used to pass a system prompt file (e.g. -c). */
|
||||
systemPromptFileConfigArg?: string;
|
||||
/** Config override key used to pass a system prompt file. */
|
||||
systemPromptFileConfigKey?: string;
|
||||
/** System prompt behavior (append vs replace). */
|
||||
systemPromptMode?: "append" | "replace";
|
||||
/** When to send system prompt. */
|
||||
|
||||
@@ -535,6 +535,8 @@ export const CliBackendSchema = z
|
||||
.optional(),
|
||||
sessionIdFields: z.array(z.string()).optional(),
|
||||
systemPromptArg: z.string().optional(),
|
||||
systemPromptFileConfigArg: z.string().optional(),
|
||||
systemPromptFileConfigKey: z.string().optional(),
|
||||
systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(),
|
||||
systemPromptWhen: z
|
||||
.union([z.literal("first"), z.literal("always"), z.literal("never")])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
@@ -110,6 +110,10 @@ vi.mock("../logging/subsystem.js", () => ({
|
||||
const { scheduleRestartSentinelWake } = await import("./server-restart-sentinel.js");
|
||||
|
||||
describe("scheduleRestartSentinelWake", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
mocks.consumeRestartSentinel.mockResolvedValue({
|
||||
@@ -178,7 +182,9 @@ describe("scheduleRestartSentinelWake", () => {
|
||||
.mockResolvedValueOnce([{ channel: "whatsapp", messageId: "msg-2" }]);
|
||||
|
||||
const wakePromise = scheduleRestartSentinelWake({ deps: {} as never });
|
||||
await vi.runAllTimersAsync();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await vi.advanceTimersByTimeAsync(750);
|
||||
await wakePromise;
|
||||
|
||||
expect(mocks.enqueueDelivery).toHaveBeenCalledTimes(1);
|
||||
@@ -218,7 +224,9 @@ describe("scheduleRestartSentinelWake", () => {
|
||||
.mockRejectedValueOnce(new Error("transport still not ready"));
|
||||
|
||||
const wakePromise = scheduleRestartSentinelWake({ deps: {} as never });
|
||||
await vi.runAllTimersAsync();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await vi.advanceTimersByTimeAsync(750);
|
||||
await wakePromise;
|
||||
|
||||
expect(mocks.enqueueDelivery).toHaveBeenCalledTimes(1);
|
||||
|
||||
Reference in New Issue
Block a user