When a non-default agent (e.g. agent:shopping-claw:main) calls restart or
config/update with sessionKey="main", the gateway treats "main" as
resolveMainSessionKey(cfg) = the default agent's session. Previously,
isTargetingOtherSession canonicalized the target key using the CURRENT
session's agentId, so "main" mapped to the current agent's main session
rather than the default agent's — falsely treating a cross-agent request
as same-session and forwarding the wrong chat's deliveryContext.
Fix: canonicalize each key using its own agentId (resolveAgentIdFromSessionKey
on the key itself). For bare "main", this returns DEFAULT_AGENT_ID so
"main" → "agent:main:main" regardless of which agent is calling. Applied
to both the restart path and the RPC path (resolveGatewayWriteMeta).
Add two regression tests covering the cross-agent alias scenario.
Forwarding liveDeliveryContextForRpc (or liveContext for restart) when
only agentChannel is set but agentTo is missing causes the server to
prefer an incomplete deliveryContext over extractDeliveryInfo(). The
sentinel is then written without `to`, and scheduleRestartSentinelWake
bails on `if (!channel || !to)`, silently degrading to a system event
with no delivery or agent resume.
Fix: guard both liveContext (restart path) and liveDeliveryContextForRpc
(config.apply/config.patch/update.run) to require both channel and to
before forwarding.
Add gateway-tool.test.ts covering the partial-context guard for both
the restart and RPC code paths.
Fixes: chatgpt-codex-connector P2 review on #34580
opts.agentThreadId belongs to the current agent's thread. When the
restart action targets a different sessionKey, forwarding it into the
sentinel would cause scheduleRestartSentinelWake to deliver the
post-restart reply to the wrong thread.
Apply the same isTargetingOtherSession guard used for deliveryContext:
only take opts.agentThreadId when the restart targets the current
session; otherwise use extracted.threadId from extractDeliveryInfo,
which correctly derives threadId from the target session key.
When a gateway tool call (restart, config.apply, config.patch, update.run)
specifies an explicit sessionKey that differs from the current agent's
session, the live delivery context (agentChannel/agentTo/agentAccountId)
belongs to the wrong session and would misroute post-restart replies.
Only set deliveryContext in the sentinel/RPC params when:
- No explicit sessionKey is provided (falls back to own session), or
- The explicit sessionKey matches the current agent's session key
Otherwise omit deliveryContext so the server falls back to
extractDeliveryInfo(sessionKey), which correctly resolves routing for
the target session.
- Extract parseDeliveryContextFromParams() into restart-request.ts and
import it in both config.ts and update.ts, eliminating the duplicated
inline IIFE parsing in update.ts
- Add comment in gateway-tool.ts explaining why agentThreadId is
intentionally excluded from liveDeliveryContextForRpc: threadId is
reliably derived server-side from the session key via
parseSessionThreadInfo() and is not subject to heartbeat contamination
- Add beforeEach(vi.clearAllMocks) to server-restart-sentinel.test.ts
and remove ad-hoc mockClear() calls from individual tests to prevent
mock state from leaking between test cases
Updates generated Swift bindings (GatewayModels.swift) to include the
deliveryContext field added to ConfigApplyLikeParamsSchema and
UpdateRunParamsSchema in src/gateway/protocol/schema/config.ts.
Fixes two related issues:
- #12768: Gateway restart notification
- #18612: Agent does not resume after self-triggered gateway restart
## Root cause
In ab4a08a82 ('fix: defer gateway restart until all replies are sent'),
agentCommand() was replaced with deliverOutboundPayloads() in
scheduleRestartSentinelWake(). This fixed a pre-restart race condition
(correct) but accidentally made delivery one-way: the user is notified
but the agent never sees the restart message and does not resume.
A compounding bug meant the sentinel was also being written with stale
routing data. extractDeliveryInfo() reads the persisted session store,
which heartbeat runs frequently overwrite to { channel: 'webchat',
to: 'heartbeat' } — an internal sink. So even restoring agentCommand()
alone would fail: the sentinel's deliveryContext was pointing nowhere.
## Fix (four parts)
**Part 1 — src/agents/openclaw-tools.ts**
Forward the live delivery fields (agentChannel, agentTo, agentThreadId,
agentAccountId) from createOpenClawTools() into createGatewayTool().
These values are captured from the current inbound message context and
are accurate; they were available at the callsite but not being passed.
**Part 2 — src/agents/tools/gateway-tool.ts**
Prefer liveDeliveryContext (built from opts.agentChannel / agentTo /
agentAccountId) over extractDeliveryInfo() when writing the sentinel.
Pass deliveryContext in config.apply, config.patch, and update.run RPC
calls so the server-side handlers receive it.
**Part 3 — src/gateway/server-restart-sentinel.ts**
Restore agentCommand() in place of deliverOutboundPayloads(). The
function runs in the new process post-restart, where there are zero
in-flight replies, so the pre-restart race condition does not apply.
agentCommand() creates a full agent turn: the restart message is
delivered to the user AND the agent sees it in its conversation history,
allowing it to resume without waiting for the user to send a new message.
**Part 4 — src/gateway/protocol/schema/config.ts**
Add deliveryContext as an optional field to ConfigApplyLikeParamsSchema
(shared by config.apply and config.patch) and UpdateRunParamsSchema.
The additionalProperties: false constraint was silently dropping the
field before it reached the server-side handlers. Also updated
resolveConfigRestartRequest() in config.ts and the update.run handler
in update.ts to prefer params.deliveryContext over extractDeliveryInfo().
## Why the heartbeat approach fails
An alternative approach (requestHeartbeatNow + enqueueSystemEvent) was
tested and rejected: the heartbeat does fire, but its delivery target
comes from the session store (lastChannel/lastTo), which reads
'webchat/heartbeat' due to heartbeat contamination. Responses route to
an internal sink and are silently dropped. agentCommand() is the correct
tool because it creates a turn with explicit delivery context attached.
- Added a test to ensure no warnings for legacy Brave config when bundled web search allowlist compatibility is applied.
- Updated validation logic to incorporate compatibility configuration for bundled web search plugins.
- Refactored the ensureRegistry function to utilize the new compatibility handling.
* test: align extension runtime mocks with plugin-sdk
Update stale extension tests to mock the plugin-sdk runtime barrels that production code now imports, and harden the Signal tool-result harness around system-event assertions so the channels lane matches current extension boundaries.
Regeneration-Prompt: |
Verify the failing channels-lane tests against current origin/main in an isolated worktree before changing anything. If the failures reproduce on main, keep the fix test-only unless production behavior is clearly wrong. Recent extension refactors moved Telegram, WhatsApp, and Signal code onto plugin-sdk runtime barrels, so update stale tests that still mock old core module paths to intercept the seams production code now uses. For Signal reaction notifications, avoid brittle assertions that depend on shared queued system-event state when a direct harness spy on enqueue behavior is sufficient. Preserve scope: only touch the failing tests and their local harness, then rerun the reproduced targeted tests plus the full channels lane and repo check gate.
* test: fix extension test drift on main
* fix: lazy-load bundled web search plugin registry
* test: make matrix sweeper failure injection portable
* fix: split heavy matrix runtime-api seams
* fix: simplify bundled web search id lookup
* test: tolerate windows env key casing
Reuse pi-ai's Anthropic client injection seam for streaming, and add
the OpenClaw-side provider discovery, auth, model catalog, and tests
needed to expose anthropic-vertex cleanly.
Signed-off-by: sallyom <somalley@redhat.com>