Skip to content

fix(agents): resolve agentId unconditionally in message tool for cron/hook sessions#36236

Closed
RealKai42 wants to merge 10 commits into
openclaw:mainfrom
RealKai42:fix/message-tool-agent-scoped-media-roots-36185
Closed

fix(agents): resolve agentId unconditionally in message tool for cron/hook sessions#36236
RealKai42 wants to merge 10 commits into
openclaw:mainfrom
RealKai42:fix/message-tool-agent-scoped-media-roots-36185

Conversation

@RealKai42

Copy link
Copy Markdown
Contributor

Summary

Fixes #36185 — message tool uses default media roots instead of agent-scoped roots for cron/hook sessions.

Root cause: In createMessageTool, the agentId passed to runMessageAction was only resolved when agentSessionKey was present:

agentId: options?.agentSessionKey
  ? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg })
  : undefined,

Cron and hook sessions often have no explicit agentSessionKey, so agentId was left undefined. This caused getAgentScopedMediaLocalRoots(cfg, undefined) to return only the default roots, rejecting workspace files with LocalMediaAccessError: path-not-allowed.

Fix: Always call resolveSessionAgentId — it falls back to the default agent ID when no session key is provided — and pass the result unconditionally.

Changes

  • src/agents/tools/message-tool.ts: extract resolvedAgentId unconditionally using resolveSessionAgentId({ sessionKey: options?.agentSessionKey, config: cfg }) so cron/hook sessions still receive the correct agent-scoped media roots.
  • src/agents/tools/message-tool.test.ts: add test verifying agentId is a non-empty string even when no session key is present.
  • CHANGELOG.md: add fix entry for message tool uses default media roots instead of agent-scoped roots #36185.

Test plan

  • pnpm test src/agents/tools/message-tool.test.ts — all 15 tests pass (including new "falls back to default agentId when no session key is present" test).
  • pnpm check — 0 warnings, 0 errors.

@openclaw-barnacle openclaw-barnacle Bot added channel: discord Channel integration: discord channel: matrix Channel integration: matrix channel: telegram Channel integration: telegram gateway Gateway runtime cli CLI command changes agents Agent runtime and tooling size: M labels Mar 5, 2026
@greptile-apps

greptile-apps Bot commented Mar 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a bug where cron/hook agent sessions used default media roots instead of agent-scoped roots by unconditionally resolving agentId in createMessageTool. The fix is correct and well-tested. The PR also bundles several unrelated improvements: Matrix rate-limit retry, SQLite WAL mode, config unrecognized-key stripping, provider model override refactoring, a signal-based stop/restart fallback for containers, and a new "disconnected" channel restart reason.

Key findings:

  • Core fix (message-tool.ts) — correct. resolveSessionAgentId always returns a string, so passing it unconditionally is safe. The new test covers the cron/hook case well.
  • Container restart fallback (lifecycle.ts) — the SIGUSR1 restart path logs "Gateway restart signal sent. Waiting for health..." but no health check is actually performed. runServiceRestart in lifecycle-core.ts returns true immediately when onNotLoaded returns true, entirely bypassing postRestartCheck / waitForGatewayHealthyRestart. The log message creates a false expectation.
  • Provider override refactoring (model.ts) — the new applyConfiguredProviderOverrides expands overrides to include api, reasoning, cost, contextWindow, maxTokens from provider/model config entries. The early-return guard correctly limits the scope to cases where provider config is actually meaningful.
  • Matrix rate-limit retry — looks correct; 30 s cap and single-retry semantics are well-tested.
  • Config key stripping (validation.ts) — the issue.keys access via type cast is a Zod internals reach-around; worth a comment noting the assumption.

Confidence Score: 3/5

  • Safe to merge after addressing the misleading health-check log in the container restart path.
  • The primary bug fix is correct and tested. The score is lowered from 5 because the container restart fallback (lifecycle.ts) logs "Waiting for health..." but the health check is actually bypassed — this is misleading to operators and could mask a failed restart in a container environment. All other changes appear sound.
  • src/cli/daemon-cli/lifecycle.ts — the SIGUSR1 restart path bypasses postRestartCheck; the log message incorrectly implies health polling occurs.

Last reviewed commit: 87da294

Comment on lines +155 to +158
if (!json) {
defaultRuntime.log(`Gateway restart signal sent. Waiting for health...`);
}
return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log message misleads — health check is never performed

After sending SIGUSR1, the code logs "Gateway restart signal sent. Waiting for health...", which implies health polling will follow. However, when onNotLoaded returns true, runServiceRestart in lifecycle-core.ts returns early (return true) immediately — entirely bypassing the postRestartCheck callback that contains waitForGatewayHealthyRestart. No health waiting actually occurs in this path.

Either:

  1. Perform a health check inside onNotLoaded before returning true (or return the port so the caller can check), or
  2. Change the log message to accurately describe what happened, e.g. "Gateway restart signal (SIGUSR1) sent." without implying a wait.
Suggested change
if (!json) {
defaultRuntime.log(`Gateway restart signal sent. Waiting for health...`);
}
return true;
if (!json) {
defaultRuntime.log(`Gateway restart signal sent.`);
}
return true;
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cli/daemon-cli/lifecycle.ts
Line: 155-158

Comment:
**Log message misleads — health check is never performed**

After sending SIGUSR1, the code logs `"Gateway restart signal sent. Waiting for health..."`, which implies health polling will follow. However, when `onNotLoaded` returns `true`, `runServiceRestart` in `lifecycle-core.ts` returns early (`return true`) immediately — entirely bypassing the `postRestartCheck` callback that contains `waitForGatewayHealthyRestart`. No health waiting actually occurs in this path.

Either:
1. Perform a health check inside `onNotLoaded` before returning `true` (or return the port so the caller can check), or
2. Change the log message to accurately describe what happened, e.g. `"Gateway restart signal (SIGUSR1) sent."` without implying a wait.

```suggestion
      if (!json) {
        defaultRuntime.log(`Gateway restart signal sent.`);
      }
      return true;
```

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87da29434d

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.filter((l) => {
const kind = classifyPortListener(l, port);
// Target listeners that look like the gateway or unknown (container pid may not show full path).
return kind === "gateway" || kind === "unknown";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict fallback signals to confirmed gateway listeners

The fallback PID selector includes listeners classified as unknown, and classifyPortListener marks any non-OpenClaw/non-SSH process as unknown; this means openclaw gateway stop/restart in no-service-manager environments can send SIGTERM/SIGUSR1 to unrelated processes that happen to own the configured port. This is a production safety issue when the port is reused by another app (or listener metadata is incomplete), because we can terminate or signal the wrong process.

Useful? React with 👍 / 👎.

Comment on lines +287 to +289
const handled = await params.onNotLoaded().catch(() => false);
if (handled) {
return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run post-restart verification after handled fallback restart

When onNotLoaded returns true, runServiceRestart exits immediately, so postRestartCheck is skipped even though the new fallback path logs "Waiting for health...". In container/no-manager flows this can report restart success without ever checking gateway health; if SIGUSR1 is ignored or restart fails, the command still returns success.

Useful? React with 👍 / 👎.

Comment thread src/config/validation.ts
Comment on lines +342 to +344
if (!target || typeof target !== "object" || Array.isArray(target)) {
target = null;
break;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow array traversal when stripping unknown config keys

The unknown-key stripping loop aborts whenever the current target is an array, so issues whose path includes an index (for example agents.list[0]) are never stripped. As a result, stale keys inside array entries still fail validation instead of being auto-ignored, which defeats the intended compatibility behavior for configs with removed keys.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

💡 Codex Review

const port = await resolveGatewayRestartPort().catch(() =>
resolveGatewayPort(loadConfig(), process.env),
);

P2 Badge Resolve stop fallback port lazily

runDaemonStop now resolves port before runServiceStop is called, which forces resolveGatewayRestartPort()/loadConfig() even when we never enter the no-service-manager fallback. If config parsing fails (or service command inspection fails and falls back to config), the stop command exits before attempting any stop path, so users can be blocked from stopping a running gateway specifically when they most need recovery from a bad config.


const handled = await params.onNotLoaded().catch(() => false);
if (handled) {
return;

P2 Badge Emit stop outcome when fallback handles not-loaded service

When onNotLoaded returns true, this branch returns immediately before emit(...) is called, so openclaw gateway stop --json can succeed with no JSON payload at all in container/no-manager flows. That breaks machine-readable CLI behavior for automation expecting a result object from stop commands.

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: discord Channel integration: discord channel: matrix Channel integration: matrix channel: telegram Channel integration: telegram cli CLI command changes gateway Gateway runtime size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

message tool uses default media roots instead of agent-scoped roots

2 participants