fix: keep group silence on no-reply path

This commit is contained in:
Peter Steinberger
2026-04-28 01:17:38 +01:00
parent 4d4c7c8ab3
commit 6b1089ffe5
5 changed files with 20 additions and 2 deletions

View File

@@ -55,7 +55,8 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- Formatting: use `oxfmt`, not Prettier. Prefer `pnpm format:check` / `pnpm format`; for targeted files use `pnpm exec oxfmt --check --threads=1 <files...>` or `pnpm exec oxfmt --write --threads=1 <files...>`.
- Linting: use repo wrappers (`pnpm lint:*`, `scripts/run-oxlint.mjs`); do not invoke generic JS formatters/lints unless a repo script uses them.
- Heavy checks: `OPENCLAW_LOCAL_CHECK=1`, mode `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`; CI/shared use `OPENCLAW_LOCAL_CHECK=0`.
- Blacksmith/Testbox is maintainer opt-in, not a repo-wide default. If Blacksmith access is available and `OPENCLAW_TESTBOX=1` is set, or a maintainer's personal AGENTS rules ask for it, use Testbox for broad, slow, Docker, live, E2E, full-suite, or CI-parity validation instead of running those heavy lanes locally. Use `OPENCLAW_LOCAL_CHECK_MODE=throttled|full` as the explicit local escape hatch.
- Blacksmith/Testbox: on maintainer machines with Blacksmith access, broad/shared validation defaults to Testbox. This includes `pnpm check`, `pnpm check:changed`, `pnpm test`, `pnpm test:changed`, Docker/E2E/live/package/build gates, and any command likely to fan out across many Vitest projects. Do not start those broad gates locally unless the user explicitly asks for local proof or sets `OPENCLAW_LOCAL_CHECK_MODE=throttled|full`.
- Local validation: targeted edit loops only, such as `pnpm test <specific-file>`, targeted formatter checks, and small lint/type probes. If a local command expands beyond targeted proof, stop it and move the broad gate to Testbox.
- Testbox use: run from repo root, pre-warm early with `blacksmith testbox warmup ci-check-testbox.yml --ref main --idle-timeout 90`, reuse the returned `tbx_...` id for all `run`/`download` commands, and stop boxes you created before handoff. Timeout bins: `90` minutes default, `240` multi-hour, `720` all-day, `1440` overnight; anything above `1440` needs explicit approval and cleanup.
- Testbox full-suite profile: `blacksmith testbox run --id <ID> "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test"`. For installable package proof, prefer the GitHub `Package Acceptance` workflow over ad hoc Testbox commands.
@@ -91,7 +92,8 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
- extension tests: extension test typecheck/tests
- public SDK/plugin contract: extension prod/test too
- unknown root/config: all lanes
- Before handoff/push for code/test/runtime/config changes: `pnpm check:changed`. Tests-only: `pnpm test:changed`. Full prod sweep: `pnpm check`.
- Before handoff/push for code/test/runtime/config changes: run `pnpm check:changed` in Testbox by default on maintainer machines. Tests-only: run `pnpm test:changed` in Testbox by default. Full prod sweep: run `pnpm check` in Testbox. Use local only for narrow targeted proof or when explicitly requested.
- If `pnpm test:changed` or `pnpm check:changed` selects broad/shared lanes, it belongs in Testbox; do not let it continue locally after it fans out.
- Docs/changelog-only and CI/workflow metadata-only changes are not changed-gate work by default. Use `git diff --check` plus the relevant formatter/docs/workflow sanity check; escalate to `pnpm check:changed` only when scripts, test config, generated docs/API, package metadata, or runtime/build behavior changed.
- Rebase sanity: after a green `pnpm check:changed`, a clean rebase onto current
`origin/main` does not require rerunning the full changed gate when the rebase

View File

@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
- Doctor/gateway services: ignore launchd/systemd companion services that only reference the gateway as a dependency, suppress inactive Linux extra-service warnings, and avoid rewriting a running systemd gateway command/entrypoint during doctor repair. Carries forward #39118. Thanks @therk.
- Daemon/service: only emit hard-coded version-manager paths such as `~/.volta/bin`, `~/.asdf/shims`, `~/.bun/bin`, and fnm/pnpm fallbacks into gateway and node service PATHs when the directories exist, so `openclaw doctor` no longer flags `gateway.path.non-minimal` against a PATH the daemon just wrote. Env-driven roots and stable user-bin dirs remain unconditional. Fixes #71944; carries forward #71964. Thanks @Sanjays2402.
- CLI/startup: disable Node's module compile cache automatically for live source-checkout launchers so in-place `pnpm build` updates are visible to the next `openclaw` CLI invocation. Fixes #73037. Thanks @LouisGameDev.
- Agents/group chat: keep silent-allowed empty and reasoning-only turns on the `NO_REPLY` path without injecting visible-answer retry prompts, and clarify the group prompt so agents use the exact silent token instead of prose. Thanks @vincentkoc.
- Agents/group chat: move `NO_REPLY` mechanics into channel-aware direct/group prompts and suppress the duplicate generic silent-reply section for auto-reply runs, so always-on group agents get one consistent stay-silent instruction. Thanks @vincentkoc.
- Providers/OpenAI: preserve encrypted empty-summary Responses reasoning items in WebSocket replay and request `reasoning.encrypted_content` on reasoning turns so GPT-5.4/GPT-5.5 sessions do not lose required `rs_*` state beside `msg_*` items. Fixes #73053. Thanks @odb36777.
- Gateway/startup: treat `plugins.enabled=false` as an early plugin fast path, skipping plugin auto-enable discovery, gateway plugin lookup/runtime-dependency staging, and stale-plugin cleanup warnings while preserving channel blocker warnings. (#73041) Thanks @WuKongAI-CMU.

View File

@@ -437,6 +437,9 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
});
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
const onlyCall = mockedRunEmbeddedAttempt.mock.calls[0]?.[0] as { prompt?: string };
expect(onlyCall.prompt).not.toContain(REASONING_ONLY_RETRY_INSTRUCTION);
expect(onlyCall.prompt).not.toContain(EMPTY_RESPONSE_RETRY_INSTRUCTION);
expect(mockedLog.warn).not.toHaveBeenCalledWith(
expect.stringContaining("reasoning-only assistant turn detected"),
);
@@ -1681,6 +1684,9 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => {
});
expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1);
const onlyCall = mockedRunEmbeddedAttempt.mock.calls[0]?.[0] as { prompt?: string };
expect(onlyCall.prompt).not.toContain(REASONING_ONLY_RETRY_INSTRUCTION);
expect(onlyCall.prompt).not.toContain(EMPTY_RESPONSE_RETRY_INSTRUCTION);
expect(result.payloads).toEqual([{ text: "NO_REPLY" }]);
expect(result.meta.terminalReplyKind).toBe("silent-empty");
expect(result.meta.livenessState).toBe("working");

View File

@@ -18,6 +18,8 @@ export function registerGroupIntroPromptCases(): void {
"Be a good group participant: mostly lurk and follow the conversation; reply only when directly addressed or you can add clear value. Emoji reactions are welcome when available. Write like a human. Avoid Markdown tables. Minimize empty lines and use normal chat conventions, not document-style spacing. Don't type literal \\n sequences; use real line breaks sparingly.";
const groupSilentNote =
'If no response is needed, reply with exactly "NO_REPLY" (and nothing else) so OpenClaw stays silent.';
const groupSilentProseGuard =
'Any prose describing silence is wrong; the whole final answer must be only "NO_REPLY".';
const cases: GroupIntroCase[] = [
{
name: "discord",
@@ -34,6 +36,7 @@ export function registerGroupIntroPromptCases(): void {
"You are in a Discord group chat.",
groupParticipationNote,
groupSilentNote,
groupSilentProseGuard,
"Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.",
],
},
@@ -51,6 +54,7 @@ export function registerGroupIntroPromptCases(): void {
"You are in a WhatsApp group chat. Your replies are automatically sent to this group chat. Do not use the message tool to send to this same group - just reply normally.",
groupParticipationNote,
groupSilentNote,
groupSilentProseGuard,
"Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.",
],
},
@@ -68,6 +72,7 @@ export function registerGroupIntroPromptCases(): void {
"You are in a Telegram group chat.",
groupParticipationNote,
groupSilentNote,
groupSilentProseGuard,
"Activation: trigger-only (you are invoked only when explicitly mentioned; recent context may be included). Address the specific sender noted in the message context.",
],
},
@@ -100,6 +105,7 @@ export function registerGroupIntroPromptCases(): void {
"Activation: always-on (you receive every group message).",
'If you only react or otherwise handle the message without a text reply, your final answer must still be exactly "NO_REPLY".',
"Never say that you are staying quiet, keeping channel noise low, making a context-only note, or sending no channel reply.",
groupSilentProseGuard,
],
defaultActivation: "always",
},

View File

@@ -256,6 +256,9 @@ export function buildGroupChatContext(params: {
lines.push(
`If you only react or otherwise handle the message without a text reply, your final answer must still be exactly "${params.silentToken}". Never say that you are staying quiet, keeping channel noise low, making a context-only note, or sending no channel reply.`,
);
lines.push(
`Any prose describing silence is wrong; the whole final answer must be only "${params.silentToken}".`,
);
}
return lines.join(" ");
}