The Carbon `GuildThreadChannel.parentId` getter throws "Cannot access rawData on partial Channel" whenever Discord delivers a partial thread (for example when an interaction channel is unhydrated). The existing `"parentId" in channel` guard did not help because the `in` operator returns true for prototype getters without invoking them, so the read still crashed `/new` and similar slash commands, guild reactions, and the native model picker when invoked from inside a thread.
Expose a `resolveDiscordChannelParentIdSafe` helper alongside the other channel accessors and use it everywhere we currently read `channel.parentId` from the inbound Discord channel. When the getter throws, the helper returns `undefined`, and the downstream code already falls back to re-fetching the thread id via `resolveDiscordChannelInfo`, keeping authorization/config lookups on the same inputs as before.
Add a regression test that installs a throwing `parentId` getter on a partial guild thread channel and asserts the slash-command path still defers and dispatches instead of surfacing an unauthorized reply.
Fixes#69861
Addresses codex P1 review on PR #69940: the previous guard rejected
targets that simply omitted accountId, but message-tool fills accountId
from the agent's bound account at exec time (message-tool.ts:730-733),
so account-bound cron jobs legitimately start with target.accountId
undefined. Rejecting that case lost skipMessagingToolDelivery, causing
dispatchCronDelivery to double-send.
Now we only reject when the tool explicitly names a *different*
accountId — which is the real CWE-284 spoof vector. Omission matches.
Tests updated accordingly:
- matcher unit test: flipped "omit accountId" case from false to true;
"accountIds differ" case preserved as the real spoof guard
- integration tests: one legitimate-default case (rewrite happens),
one explicit-mismatch case (rewrite suppressed)
658 cron tests pass.
When a cron job sends via the generic `message` tool, the delivery trace
previously recorded `messageToolSentTo[i].channel = "message"` even
though the send was resolved to a specific channel (e.g. telegram). This
made `jq` diffing intended-vs-actual awkward for the happy path.
Fix:
- `normalizeMessagingToolTarget` now rewrites `channel: "message"`
to the resolved channel when `matchesMessagingToolDeliveryTarget`
confirms the tool send matches the resolved cron delivery target.
Genuinely unmatched generic sends keep the literal "message" so
audits can still flag them.
- `matchesMessagingToolDeliveryTarget` now requires strict accountId
equality whenever the resolved delivery carries an `accountId`. An
omitted `target.accountId` previously short-circuited the guard and
was treated as a wildcard, letting a generic send spoof attribution to
any bot identity in the cron delivery trace (CWE-284). This was
flagged by Aisle on #69771.
Tests:
- Unit: `matchesMessagingToolDeliveryTarget` rejects omitted-accountId
against account-tied delivery; still matches same-accountId.
- Integration: cron run trace rewrites generic "message" to the
resolved channel, preserves accountId on both sides, and leaves the
literal "message" provider in place when the tool send omits
accountId against an account-tied delivery.
navigateChromeMcpPage() now always passes a timeout to the Chrome MCP
navigate_page tool (defaulting to CHROME_MCP_NAVIGATE_TIMEOUT_MS when
the caller omits timeoutMs), and callTool() grows an optional safety-net
that tears down a stuck session via Promise.race so the next caller gets
a fresh subprocess. The catch block gains a transport-identity guard to
avoid clobbering a concurrently-created replacement session.
* fix(memory/dreaming): surface blocked status in memory status when heartbeat disabled for main
Replace the hand-rolled heartbeat-rules logic in resolveDreamingBlockedReason
with the shared resolveHeartbeatSummaryForAgent helper, promoted from core to
the plugin-sdk via infra-runtime. Collapses the two disabled-reason branches
into a single message that points at a new Troubleshooting section in the
dreaming docs, so the silent-failure mode described in openclaw/openclaw#69843
becomes legible without the extension re-encoding heartbeat-enablement rules.
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* refactor(memory/dreaming): share resolveDreamingBlockedReason across cli and /dreaming surfaces
- Move resolveDreamingBlockedReason from cli.runtime.ts into dreaming.ts as an exported helper and pin its heartbeat check to DEFAULT_AGENT_ID (now exported from plugin-sdk/routing) so the status-line check agrees with the cron's hardcoded sessionTarget even when the configured default agent is not main.
- Render the blocked reason from formatStatus in dreaming-command.ts directly under the enabled line, so /dreaming status, /dreaming on, /dreaming off, and bare /dreaming all flag that the cron is blocked instead of implying dreaming is healthy.
- Tighten the blocked-reason text to lead with user impact ('dreaming is enabled but will not run because heartbeat is disabled for main'), so operators immediately understand the config is toggled on but nothing is actually running.
- Tighten the dreaming Troubleshooting copy to name main explicitly and mention both surfaces.
- Add tests locking the new behavior across cli.test.ts (default-agent=ops still reports blocked for main) and dreaming-command.test.ts (/dreaming status ordering, /dreaming on surfacing, healthy-heartbeat omission).
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* fix(memory/dreaming): check heartbeat for the resolved default agent, not the literal 'main'
sessionTarget: 'main' is a cron session-type enum variant meaning 'the default agent's main session', not an agent id (see src/cron/service/jobs.ts). buildManagedDreamingCronJob does not set agentId, and cron runtime resolves the missing agentId through resolveDefaultAgentId(cfg) before enqueuing or waking. The previous pin to DEFAULT_AGENT_ID could produce a false 'blocked' reading when a configured default agent is not 'main' and its heartbeat is fine, and could miss a real block when the default agent is not 'main' and that agent's heartbeat is actually off.
Switch resolveDreamingBlockedReason to resolveDefaultAgentId(cfg) and interpolate the resolved agent id into the message so the blocked line names the agent whose heartbeat is the blocker. Introduce a narrow local CRON_SESSION_TARGET_MAIN constant for the cron session-type enum variant (used by the sessionTarget type and value) so the remaining 'main' literal is semantically distinct from any agent id. Revert the DEFAULT_AGENT_ID export addition on plugin-sdk/routing; memory-core no longer needs it. Update the Troubleshooting doc wording and the cli test that was locking the wrong behaviour.
Refs openclaw/openclaw#69843, openclaw/openclaw#46046.
* fix(memory/dreaming): align blocked check with server-cron wake's defaults-only heartbeat
resolveDreamingBlockedReason was using resolveHeartbeatSummaryForAgent, which merges agents.defaults.heartbeat with agents.list[].heartbeat. The managed dreaming cron leaves job.agentId and job.sessionKey unset, so server-cron's wake wrapper cannot look up a per-agent entry and calls runHeartbeatOnce with agents.defaults.heartbeat only. Using the summary helper would disagree with the actual wake when the default agent overrides heartbeat.every differently from the defaults (either direction — false blocked when the override would run, or false healthy when defaults block).
Mirror the wake path explicitly: rule-1 enablement via isHeartbeatEnabledForAgent against the default agent, rule-3 interval via resolveHeartbeatIntervalMs with defaults-only heartbeat config. Comment points at server-cron so a future cleanup of that latent override-propagation gap sees the coupling.
Refs openclaw/openclaw#69843.
Link docs feature cards to their intended destination pages in the English docs surfaces.
- add hrefs to the feature cards in docs/concepts/features.md
- add hrefs to the key capability cards in docs/index.md
- preserve current main branch copy while landing the navigation fix