fix(cli-runner): fire before_agent_reply on cron-triggered turns#70950
Conversation
Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts for the CLI runner. Asserts: 1. When trigger=cron and a before_agent_reply hook claims the turn (handled: true), runCliAgent must NOT invoke the codex subprocess and must return the hook's reply text in payloads[0]. 2. When the hook claims without a reply body, the synthesized payload uses SILENT_REPLY_TOKEN. 3. Non-cron triggers do not invoke the hook (no behavior change for normal user/heartbeat traffic). 4. Without a registered hook, falls through to the CLI subprocess. Currently fails (RED): tests 1 and 2 fail because runCliAgent never fires before_agent_reply — the hook gate exists only in the embedded PI runner (src/agents/pi-embedded-runner/run.ts:326). This is the CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in PR openclaw#70737 review. Next commit: implement the hook gate in runPreparedCliAgent (GREEN).
8be920b to
6d2c3da
Compare
Greptile SummaryThis PR ports the cron-triggered
Confidence Score: 3/5Mergeable in principle, but the cron gate early-return skips backend cleanup and will leak MCP config resources on each handled dreaming turn. The functional fix is correct and the tests are thorough. However, the gate placement inside src/agents/cli-runner.ts — specifically the placement of the cron gate relative to resource allocation and the Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/agents/cli-runner.ts
Line: 117-137
Comment:
**Cron gate skips `preparedBackend.cleanup`, leaking resources**
`prepareCliRunContext` may allocate real resources — MCP bundle config temp files and/or backend execution cleanup closures via `backendResolved.prepareExecution` — that are meant to be released in the `try…finally` block at the bottom of `runPreparedCliAgent`. The early return here exits before that block, so every cron turn the hook handles will silently skip cleanup.
The embedded PI runner's equivalent gate (`run.ts:334`) fires before any resource setup, so it doesn't have this problem. The safest fix is to hoist this check into `runCliAgent` before `prepareCliRunContext` is called, or to call `await context.preparedBackend.cleanup?.()` in a `try…finally` wrapping the early return.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(cli-runner): GREEN — fire before_age..." | Re-trigger Greptile |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6d2c3daa1d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
9dc1cfb to
f0d11be
Compare
…urns Mirrors the embedded PI runner gate from src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs (notably memory-core dreaming) can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Without this, configuring a default agent's model to a CLI backend (codex-cli, claude-cli, gemini-cli, or any third-party `registerCliBackend` provider) silently broke dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed. See openclaw#70940 for the empirical repro (codex-cli observed sending the dream-token to GPT-5.5 with no `memory-core: dreaming promotion complete` line). Also extracts `buildHandledReplyPayloads` locally; eventually that should be unified with the embedded PI runner's helper, but that's a mechanical refactor for a follow-up. Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only useful if cron-driven dreaming exists, which is what openclaw#70737 introduces. TDD trail: - prior commit: RED test asserting the hook gate (4 cases) - this commit: implementation that turns those tests green (4/4 pass). Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts 4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean on touched files; pre-existing tsgo failure in src/plugin-sdk/provider-tools.ts is unrelated to these changes.
f0d11be to
2eeeb4b
Compare
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟡 Sensitive sessionKey and filesystem path exposed to before_agent_reply plugin hooks (cron gate)
DescriptionIn If plugin hooks can be authored/installed by third parties (or run in otherwise less-trusted environments), this is a data exposure risk:
Vulnerable code: const hookContext = {
runId: params.runId,
agentId: params.agentId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
workspaceDir: params.workspaceDir,
...
} as const;
const hookResult = await hookRunner.runBeforeAgentReply(
{ cleanedBody: params.prompt },
hookContext,
);RecommendationMinimize the data exposed to
Example (omit sensitive fields): const hookContext = {
runId: params.runId,
agentId: params.agentId,
sessionId: params.sessionId,
messageProvider: params.messageProvider,
trigger: params.trigger,
channelId: params.messageChannel ?? params.messageProvider,
} as const;If retaining these fields is necessary, document that hooks are fully trusted and consider adding a configuration flag that disables passing sensitive context to hooks by default. Analyzed PR: #70950 at commit Last updated on: 2026-04-24T05:59:11Z |
…nclaw#70950) * test(cli-runner): RED — assert before_agent_reply fires on cron triggers Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts for the CLI runner. Asserts: 1. When trigger=cron and a before_agent_reply hook claims the turn (handled: true), runCliAgent must NOT invoke the codex subprocess and must return the hook's reply text in payloads[0]. 2. When the hook claims without a reply body, the synthesized payload uses SILENT_REPLY_TOKEN. 3. Non-cron triggers do not invoke the hook (no behavior change for normal user/heartbeat traffic). 4. Without a registered hook, falls through to the CLI subprocess. Currently fails (RED): tests 1 and 2 fail because runCliAgent never fires before_agent_reply — the hook gate exists only in the embedded PI runner (src/agents/pi-embedded-runner/run.ts:326). This is the CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in PR openclaw#70737 review. Next commit: implement the hook gate in runPreparedCliAgent (GREEN). * fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns Mirrors the embedded PI runner gate from src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs (notably memory-core dreaming) can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Without this, configuring a default agent's model to a CLI backend (codex-cli, claude-cli, gemini-cli, or any third-party `registerCliBackend` provider) silently broke dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed. See openclaw#70940 for the empirical repro (codex-cli observed sending the dream-token to GPT-5.5 with no `memory-core: dreaming promotion complete` line). Also extracts `buildHandledReplyPayloads` locally; eventually that should be unified with the embedded PI runner's helper, but that's a mechanical refactor for a follow-up. Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only useful if cron-driven dreaming exists, which is what openclaw#70737 introduces. TDD trail: - prior commit: RED test asserting the hook gate (4 cases) - this commit: implementation that turns those tests green (4/4 pass). Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts 4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean on touched files; pre-existing tsgo failure in src/plugin-sdk/provider-tools.ts is unrelated to these changes.
…nclaw#70950) * test(cli-runner): RED — assert before_agent_reply fires on cron triggers Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts for the CLI runner. Asserts: 1. When trigger=cron and a before_agent_reply hook claims the turn (handled: true), runCliAgent must NOT invoke the codex subprocess and must return the hook's reply text in payloads[0]. 2. When the hook claims without a reply body, the synthesized payload uses SILENT_REPLY_TOKEN. 3. Non-cron triggers do not invoke the hook (no behavior change for normal user/heartbeat traffic). 4. Without a registered hook, falls through to the CLI subprocess. Currently fails (RED): tests 1 and 2 fail because runCliAgent never fires before_agent_reply — the hook gate exists only in the embedded PI runner (src/agents/pi-embedded-runner/run.ts:326). This is the CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in PR openclaw#70737 review. Next commit: implement the hook gate in runPreparedCliAgent (GREEN). * fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns Mirrors the embedded PI runner gate from src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs (notably memory-core dreaming) can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Without this, configuring a default agent's model to a CLI backend (codex-cli, claude-cli, gemini-cli, or any third-party `registerCliBackend` provider) silently broke dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed. See openclaw#70940 for the empirical repro (codex-cli observed sending the dream-token to GPT-5.5 with no `memory-core: dreaming promotion complete` line). Also extracts `buildHandledReplyPayloads` locally; eventually that should be unified with the embedded PI runner's helper, but that's a mechanical refactor for a follow-up. Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only useful if cron-driven dreaming exists, which is what openclaw#70737 introduces. TDD trail: - prior commit: RED test asserting the hook gate (4 cases) - this commit: implementation that turns those tests green (4/4 pass). Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts 4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean on touched files; pre-existing tsgo failure in src/plugin-sdk/provider-tools.ts is unrelated to these changes.
…nclaw#70950) * test(cli-runner): RED — assert before_agent_reply fires on cron triggers Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts for the CLI runner. Asserts: 1. When trigger=cron and a before_agent_reply hook claims the turn (handled: true), runCliAgent must NOT invoke the codex subprocess and must return the hook's reply text in payloads[0]. 2. When the hook claims without a reply body, the synthesized payload uses SILENT_REPLY_TOKEN. 3. Non-cron triggers do not invoke the hook (no behavior change for normal user/heartbeat traffic). 4. Without a registered hook, falls through to the CLI subprocess. Currently fails (RED): tests 1 and 2 fail because runCliAgent never fires before_agent_reply — the hook gate exists only in the embedded PI runner (src/agents/pi-embedded-runner/run.ts:326). This is the CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in PR openclaw#70737 review. Next commit: implement the hook gate in runPreparedCliAgent (GREEN). * fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns Mirrors the embedded PI runner gate from src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs (notably memory-core dreaming) can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Without this, configuring a default agent's model to a CLI backend (codex-cli, claude-cli, gemini-cli, or any third-party `registerCliBackend` provider) silently broke dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed. See openclaw#70940 for the empirical repro (codex-cli observed sending the dream-token to GPT-5.5 with no `memory-core: dreaming promotion complete` line). Also extracts `buildHandledReplyPayloads` locally; eventually that should be unified with the embedded PI runner's helper, but that's a mechanical refactor for a follow-up. Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only useful if cron-driven dreaming exists, which is what openclaw#70737 introduces. TDD trail: - prior commit: RED test asserting the hook gate (4 cases) - this commit: implementation that turns those tests green (4/4 pass). Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts 4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean on touched files; pre-existing tsgo failure in src/plugin-sdk/provider-tools.ts is unrelated to these changes.
…nclaw#70950) * test(cli-runner): RED — assert before_agent_reply fires on cron triggers Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts for the CLI runner. Asserts: 1. When trigger=cron and a before_agent_reply hook claims the turn (handled: true), runCliAgent must NOT invoke the codex subprocess and must return the hook's reply text in payloads[0]. 2. When the hook claims without a reply body, the synthesized payload uses SILENT_REPLY_TOKEN. 3. Non-cron triggers do not invoke the hook (no behavior change for normal user/heartbeat traffic). 4. Without a registered hook, falls through to the CLI subprocess. Currently fails (RED): tests 1 and 2 fail because runCliAgent never fires before_agent_reply — the hook gate exists only in the embedded PI runner (src/agents/pi-embedded-runner/run.ts:326). This is the CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in PR openclaw#70737 review. Next commit: implement the hook gate in runPreparedCliAgent (GREEN). * fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns Mirrors the embedded PI runner gate from src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs (notably memory-core dreaming) can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Without this, configuring a default agent's model to a CLI backend (codex-cli, claude-cli, gemini-cli, or any third-party `registerCliBackend` provider) silently broke dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed. See openclaw#70940 for the empirical repro (codex-cli observed sending the dream-token to GPT-5.5 with no `memory-core: dreaming promotion complete` line). Also extracts `buildHandledReplyPayloads` locally; eventually that should be unified with the embedded PI runner's helper, but that's a mechanical refactor for a follow-up. Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only useful if cron-driven dreaming exists, which is what openclaw#70737 introduces. TDD trail: - prior commit: RED test asserting the hook gate (4 cases) - this commit: implementation that turns those tests green (4/4 pass). Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts 4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean on touched files; pre-existing tsgo failure in src/plugin-sdk/provider-tools.ts is unrelated to these changes.
…nclaw#70950) * test(cli-runner): RED — assert before_agent_reply fires on cron triggers Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts for the CLI runner. Asserts: 1. When trigger=cron and a before_agent_reply hook claims the turn (handled: true), runCliAgent must NOT invoke the codex subprocess and must return the hook's reply text in payloads[0]. 2. When the hook claims without a reply body, the synthesized payload uses SILENT_REPLY_TOKEN. 3. Non-cron triggers do not invoke the hook (no behavior change for normal user/heartbeat traffic). 4. Without a registered hook, falls through to the CLI subprocess. Currently fails (RED): tests 1 and 2 fail because runCliAgent never fires before_agent_reply — the hook gate exists only in the embedded PI runner (src/agents/pi-embedded-runner/run.ts:326). This is the CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in PR openclaw#70737 review. Next commit: implement the hook gate in runPreparedCliAgent (GREEN). * fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns Mirrors the embedded PI runner gate from src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs (notably memory-core dreaming) can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Without this, configuring a default agent's model to a CLI backend (codex-cli, claude-cli, gemini-cli, or any third-party `registerCliBackend` provider) silently broke dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed. See openclaw#70940 for the empirical repro (codex-cli observed sending the dream-token to GPT-5.5 with no `memory-core: dreaming promotion complete` line). Also extracts `buildHandledReplyPayloads` locally; eventually that should be unified with the embedded PI runner's helper, but that's a mechanical refactor for a follow-up. Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only useful if cron-driven dreaming exists, which is what openclaw#70737 introduces. TDD trail: - prior commit: RED test asserting the hook gate (4 cases) - this commit: implementation that turns those tests green (4/4 pass). Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts 4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean on touched files; pre-existing tsgo failure in src/plugin-sdk/provider-tools.ts is unrelated to these changes.
…nclaw#70950) * test(cli-runner): RED — assert before_agent_reply fires on cron triggers Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts for the CLI runner. Asserts: 1. When trigger=cron and a before_agent_reply hook claims the turn (handled: true), runCliAgent must NOT invoke the codex subprocess and must return the hook's reply text in payloads[0]. 2. When the hook claims without a reply body, the synthesized payload uses SILENT_REPLY_TOKEN. 3. Non-cron triggers do not invoke the hook (no behavior change for normal user/heartbeat traffic). 4. Without a registered hook, falls through to the CLI subprocess. Currently fails (RED): tests 1 and 2 fail because runCliAgent never fires before_agent_reply — the hook gate exists only in the embedded PI runner (src/agents/pi-embedded-runner/run.ts:326). This is the CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in PR openclaw#70737 review. Next commit: implement the hook gate in runPreparedCliAgent (GREEN). * fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns Mirrors the embedded PI runner gate from src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs (notably memory-core dreaming) can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Without this, configuring a default agent's model to a CLI backend (codex-cli, claude-cli, gemini-cli, or any third-party `registerCliBackend` provider) silently broke dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed. See openclaw#70940 for the empirical repro (codex-cli observed sending the dream-token to GPT-5.5 with no `memory-core: dreaming promotion complete` line). Also extracts `buildHandledReplyPayloads` locally; eventually that should be unified with the embedded PI runner's helper, but that's a mechanical refactor for a follow-up. Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only useful if cron-driven dreaming exists, which is what openclaw#70737 introduces. TDD trail: - prior commit: RED test asserting the hook gate (4 cases) - this commit: implementation that turns those tests green (4/4 pass). Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts 4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean on touched files; pre-existing tsgo failure in src/plugin-sdk/provider-tools.ts is unrelated to these changes.
…nclaw#70950) * test(cli-runner): RED — assert before_agent_reply fires on cron triggers Mirrors src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts for the CLI runner. Asserts: 1. When trigger=cron and a before_agent_reply hook claims the turn (handled: true), runCliAgent must NOT invoke the codex subprocess and must return the hook's reply text in payloads[0]. 2. When the hook claims without a reply body, the synthesized payload uses SILENT_REPLY_TOKEN. 3. Non-cron triggers do not invoke the hook (no behavior change for normal user/heartbeat traffic). 4. Without a registered hook, falls through to the CLI subprocess. Currently fails (RED): tests 1 and 2 fail because runCliAgent never fires before_agent_reply — the hook gate exists only in the embedded PI runner (src/agents/pi-embedded-runner/run.ts:326). This is the CLI-backed-agent dreaming gap reported in openclaw#70940 and identified in PR openclaw#70737 review. Next commit: implement the hook gate in runPreparedCliAgent (GREEN). * fix(cli-runner): GREEN — fire before_agent_reply for cron-triggered turns Mirrors the embedded PI runner gate from src/agents/pi-embedded-runner/run.ts:326 so plugin-managed cron jobs (notably memory-core dreaming) can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Without this, configuring a default agent's model to a CLI backend (codex-cli, claude-cli, gemini-cli, or any third-party `registerCliBackend` provider) silently broke dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed. See openclaw#70940 for the empirical repro (codex-cli observed sending the dream-token to GPT-5.5 with no `memory-core: dreaming promotion complete` line). Also extracts `buildHandledReplyPayloads` locally; eventually that should be unified with the embedded PI runner's helper, but that's a mechanical refactor for a follow-up. Closes openclaw#70940 once both this PR and openclaw#70737 land — this fix is only useful if cron-driven dreaming exists, which is what openclaw#70737 introduces. TDD trail: - prior commit: RED test asserting the hook gate (4 cases) - this commit: implementation that turns those tests green (4/4 pass). Verified: pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts 4/4 passed; pnpm test src/agents/cli-runner 21/21 passed; lint clean on touched files; pre-existing tsgo failure in src/plugin-sdk/provider-tools.ts is unrelated to these changes.
Summary
Adds a
before_agent_replycall site torunPreparedCliAgentso plugin-managed cron jobs can short-circuit a CLI-backed agent turn before the codex/claude/gemini subprocess is spawned. Mirrors the embedded PI runner gate atsrc/agents/pi-embedded-runner/run.ts:326.Without this, configuring a default agent's model to a CLI backend silently broke memory-core dreaming: the cron sentinel was sent to the underlying LLM as a literal user prompt and the dreaming hook never executed.
Closes #70940.
TDD trail
8998e9b5c6— RED: 4-case test mirroringpi-embedded-runner/run.before-agent-reply-cron.test.ts. Two cases fail without the gate; two pass trivially.9dc1cfbfb6— GREEN: hook gate added torunPreparedCliAgent. 4/4 pass.Empirical validation (manual e2e)
Reran the same isolated-gateway repro from #70940 against this branch (
OPENCLAW_HOME=/tmp/dream-test-cli,model: { primary: "codex-cli/gpt-5.5" }, dreaming enabled at*/2 * * * *).memory-core: dreaming promotion completelinespromptChars=217)The fast natural firings are the unambiguous proof: hook fires, returns
handled: true, cli subprocess is skipped entirely.Test plan
pnpm test src/agents/cli-runner.before-agent-reply-cron.test.ts— 4/4pnpm test src/agents/cli-runner— 21/21Note
buildHandledReplyPayloadsis now duplicated incli-runner.tsandpi-embedded-runner/run.ts. Worth a follow-up to extract; out of scope here to keep the diff tight.