diff --git a/AGENTS.md b/AGENTS.md index 0c1025b1c5f..2038431c980 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 ` or `pnpm exec oxfmt --write --threads=1 `. - 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 `, 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 "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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e1b3bbc216b..c9a3a055c60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts index 312fab4802d..57c1289f145 100644 --- a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts +++ b/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts @@ -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"); diff --git a/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts b/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts index 7e393a1c55e..78402323e18 100644 --- a/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts +++ b/src/auto-reply/reply.triggers.group-intro-prompts.cases.ts @@ -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", }, diff --git a/src/auto-reply/reply/groups.ts b/src/auto-reply/reply/groups.ts index 84cbd55c316..47257a52184 100644 --- a/src/auto-reply/reply/groups.ts +++ b/src/auto-reply/reply/groups.ts @@ -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(" "); }