feat(wake): expose typed sessionKey on wake protocol + system event CLI (refs #52305)#78687
Conversation
|
Codex review: needs changes before merge. Summary Reproducibility: yes. Source inspection on current main shows Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Land the focused external wake/sessionKey surface after aligning the fallback wording or behavior and coordinating with the companion internal cron-run routing PR. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection on current main shows Is this the best way to solve the issue? Mostly yes. The additive protocol/CLI field and existing CronService/gateway adapter threading are the narrow maintainable path, but the docs or unknown-agent fallback behavior should be made consistent before merge. Full review comments:
Overall correctness: patch is correct Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against e432c3270114. |
6454d57 to
e539325
Compare
25db9de to
02e4561
Compare
Codex review on PR openclaw#78687 [P3] flagged that the docs say next-heartbeat "waits for the next scheduled tick" while the patched timer collapses next-heartbeat+sessionKey to an immediate targeted wake. Add a callout describing the exception and pointing callers who want delayed delivery back at the no-session-key path. Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
2026-05-07 — addressed clawsweeper review feedback Both findings from the prior review are addressed:
Locally: CI is currently re-running the broad shards on the rebased branch. |
…e and wake When `cron.wake` is called with only an agent-prefixed `sessionKey` (no explicit `agentId`), the gateway cron adapter must derive the same agentId on both `enqueueSystemEvent` and `requestHeartbeat` so events land in (and heartbeats fire on) the same agent target. Pre-PR, only `requestHeartbeat` derived agentId from the key; `enqueueSystemEvent` ran through `resolveCronSessionKey` with the configured-default agent and was rerouted to that agent's main session under multi-agent deployments where `main` exists but is not the default. The new test exercises the cron-adapter directly via `state.cron.state.deps` with a multi-agent config (`primary` default + `ops` non-default) and a `agent:ops:cron:nightly:run:abc-123` foreign-agent session key, asserting that both call sites resolve the agent target to "ops" rather than falling back to "primary". Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
2026-05-07 — added cron-adapter integration test + live gateway smoke Followed up on Bossman's request to add stronger evidence than the helper-script proof:
The corresponding foreign-agent live observation didn't surface — heartbeat-runner is gated on per-agent model/heartbeat config, and the test config's Body updated; "What was not tested" now reflects the dispatch-layer gating limitation honestly rather than the prior blanket "live E2E not run" caveat. Re-review progress:
|
|
2026-05-07 — completed live-gateway proof of foreign-agent routing Per Bossman's call to add a real model + heartbeat config to the test setup so the foreign-agent dispatch lane surfaces. Root cause of the prior gap: Fixed by adding The lane name is the routing-decision proof. Foreign-agent call (CASE_A3) routed to PR body updated (Live-gateway smoke section + Test plan checklist) with the captured logs. The routing-decision signal is the lane name, captured upstream of model invocation. Re-review progress:
|
cbded2f to
fdb5530
Compare
Codex review on PR openclaw#78687 [P3] flagged that the docs say next-heartbeat "waits for the next scheduled tick" while the patched timer collapses next-heartbeat+sessionKey to an immediate targeted wake. Add a callout describing the exception and pointing callers who want delayed delivery back at the no-session-key path. Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e and wake When `cron.wake` is called with only an agent-prefixed `sessionKey` (no explicit `agentId`), the gateway cron adapter must derive the same agentId on both `enqueueSystemEvent` and `requestHeartbeat` so events land in (and heartbeats fire on) the same agent target. Pre-PR, only `requestHeartbeat` derived agentId from the key; `enqueueSystemEvent` ran through `resolveCronSessionKey` with the configured-default agent and was rerouted to that agent's main session under multi-agent deployments where `main` exists but is not the default. The new test exercises the cron-adapter directly via `state.cron.state.deps` with a multi-agent config (`primary` default + `ops` non-default) and a `agent:ops:cron:nightly:run:abc-123` foreign-agent session key, asserting that both call sites resolve the agent target to "ops" rather than falling back to "primary". Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Updated this PR for current Changes since the earlier revision:
Validation:
|
Adds an optional sessionKey to the WakeParamsSchema and threads it through the gateway wake handler, CronService.wake(), and the underlying timer.wake() ops so callers can target a specific session for async-task completion relays instead of always hitting the agent's main session. Also adds --session-key to `openclaw system event`. The schema rejects empty/non-string sessionKey at the gateway boundary; mismatched session keys (a key that does not belong to the resolving agent) fall back to the agent's main session inside resolveCronSessionKey, which is the existing safety path. Refs openclaw#52305 (companion to PR openclaw#50818, which closes the related cron-run remap slice at internal enqueue sites). Doesn't depend on openclaw#50818. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… in cron adapter Address review findings from successive codex rounds: 1. next-heartbeat + sessionKey now fires a targeted immediate wake. The regularly-scheduled heartbeat fires for the agent's main session, not the supplied sessionKey, so an event queued for a non-main session would sit stranded indefinitely; an "event"-intent wake is also deferred as not-due by the heartbeat runner and not retried, so neither path delivers without an explicit immediate wake. 2. resolveCronWakeTarget now always runs through resolveCronAgent, both for agent-prefixed session keys (so non-default agents are honored) and relative keys (so the configured default agent is used instead of the hardcoded "main" returned by resolveAgentIdFromSessionKey). Mirrors the matching fix in the enqueueSystemEvent adapter so wake and enqueue resolve to the same target. 3. Generated Swift `WakeParams` models now expose the new optional `sessionkey` field (codingKey "sessionKey") in both the macOS and shared OpenClawKit copies. Locally regenerated from agent.ts via protocol:gen + protocol:gen:swift would have produced this; the environment couldn't run the generators (fs-safe transitive typecheck errors), so the diff was applied by hand to match what pnpm protocol:check would output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Caught by oxlint typescript-eslint(no-unnecessary-type-assertion) in CI. mock.calls is typed as any[][], so the trailing `!` adds nothing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review on PR openclaw#78687 [P3] flagged that the docs say next-heartbeat "waits for the next scheduled tick" while the patched timer collapses next-heartbeat+sessionKey to an immediate targeted wake. Add a callout describing the exception and pointing callers who want delayed delivery back at the no-session-key path. Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e and wake When `cron.wake` is called with only an agent-prefixed `sessionKey` (no explicit `agentId`), the gateway cron adapter must derive the same agentId on both `enqueueSystemEvent` and `requestHeartbeat` so events land in (and heartbeats fire on) the same agent target. Pre-PR, only `requestHeartbeat` derived agentId from the key; `enqueueSystemEvent` ran through `resolveCronSessionKey` with the configured-default agent and was rerouted to that agent's main session under multi-agent deployments where `main` exists but is not the default. The new test exercises the cron-adapter directly via `state.cron.state.deps` with a multi-agent config (`primary` default + `ops` non-default) and a `agent:ops:cron:nightly:run:abc-123` foreign-agent session key, asserting that both call sites resolve the agent target to "ops" rather than falling back to "primary". Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fa93db7 to
8a5065c
Compare
Codex review on PR #78687 [P3] flagged that the docs say next-heartbeat "waits for the next scheduled tick" while the patched timer collapses next-heartbeat+sessionKey to an immediate targeted wake. Add a callout describing the exception and pointing callers who want delayed delivery back at the no-session-key path. Refs #78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e and wake When `cron.wake` is called with only an agent-prefixed `sessionKey` (no explicit `agentId`), the gateway cron adapter must derive the same agentId on both `enqueueSystemEvent` and `requestHeartbeat` so events land in (and heartbeats fire on) the same agent target. Pre-PR, only `requestHeartbeat` derived agentId from the key; `enqueueSystemEvent` ran through `resolveCronSessionKey` with the configured-default agent and was rerouted to that agent's main session under multi-agent deployments where `main` exists but is not the default. The new test exercises the cron-adapter directly via `state.cron.state.deps` with a multi-agent config (`primary` default + `ops` non-default) and a `agent:ops:cron:nightly:run:abc-123` foreign-agent session key, asserting that both call sites resolve the agent target to "ops" rather than falling back to "primary". Refs #78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Landed via rebase onto Proof used before merge:
Maintainer fixup source SHA: 4d75876 Thanks @Kaspre! |
Codex review on PR #78687 [P3] flagged that the docs say next-heartbeat "waits for the next scheduled tick" while the patched timer collapses next-heartbeat+sessionKey to an immediate targeted wake. Add a callout describing the exception and pointing callers who want delayed delivery back at the no-session-key path. Refs #78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e and wake When `cron.wake` is called with only an agent-prefixed `sessionKey` (no explicit `agentId`), the gateway cron adapter must derive the same agentId on both `enqueueSystemEvent` and `requestHeartbeat` so events land in (and heartbeats fire on) the same agent target. Pre-PR, only `requestHeartbeat` derived agentId from the key; `enqueueSystemEvent` ran through `resolveCronSessionKey` with the configured-default agent and was rerouted to that agent's main session under multi-agent deployments where `main` exists but is not the default. The new test exercises the cron-adapter directly via `state.cron.state.deps` with a multi-agent config (`primary` default + `ops` non-default) and a `agent:ops:cron:nightly:run:abc-123` foreign-agent session key, asserting that both call sites resolve the agent target to "ops" rather than falling back to "primary". Refs #78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review on PR openclaw#78687 [P3] flagged that the docs say next-heartbeat "waits for the next scheduled tick" while the patched timer collapses next-heartbeat+sessionKey to an immediate targeted wake. Add a callout describing the exception and pointing callers who want delayed delivery back at the no-session-key path. Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e and wake When `cron.wake` is called with only an agent-prefixed `sessionKey` (no explicit `agentId`), the gateway cron adapter must derive the same agentId on both `enqueueSystemEvent` and `requestHeartbeat` so events land in (and heartbeats fire on) the same agent target. Pre-PR, only `requestHeartbeat` derived agentId from the key; `enqueueSystemEvent` ran through `resolveCronSessionKey` with the configured-default agent and was rerouted to that agent's main session under multi-agent deployments where `main` exists but is not the default. The new test exercises the cron-adapter directly via `state.cron.state.deps` with a multi-agent config (`primary` default + `ops` non-default) and a `agent:ops:cron:nightly:run:abc-123` foreign-agent session key, asserting that both call sites resolve the agent target to "ops" rather than falling back to "primary". Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review on PR openclaw#78687 [P3] flagged that the docs say next-heartbeat "waits for the next scheduled tick" while the patched timer collapses next-heartbeat+sessionKey to an immediate targeted wake. Add a callout describing the exception and pointing callers who want delayed delivery back at the no-session-key path. Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e and wake When `cron.wake` is called with only an agent-prefixed `sessionKey` (no explicit `agentId`), the gateway cron adapter must derive the same agentId on both `enqueueSystemEvent` and `requestHeartbeat` so events land in (and heartbeats fire on) the same agent target. Pre-PR, only `requestHeartbeat` derived agentId from the key; `enqueueSystemEvent` ran through `resolveCronSessionKey` with the configured-default agent and was rerouted to that agent's main session under multi-agent deployments where `main` exists but is not the default. The new test exercises the cron-adapter directly via `state.cron.state.deps` with a multi-agent config (`primary` default + `ops` non-default) and a `agent:ops:cron:nightly:run:abc-123` foreign-agent session key, asserting that both call sites resolve the agent target to "ops" rather than falling back to "primary". Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review on PR openclaw#78687 [P3] flagged that the docs say next-heartbeat "waits for the next scheduled tick" while the patched timer collapses next-heartbeat+sessionKey to an immediate targeted wake. Add a callout describing the exception and pointing callers who want delayed delivery back at the no-session-key path. Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e and wake When `cron.wake` is called with only an agent-prefixed `sessionKey` (no explicit `agentId`), the gateway cron adapter must derive the same agentId on both `enqueueSystemEvent` and `requestHeartbeat` so events land in (and heartbeats fire on) the same agent target. Pre-PR, only `requestHeartbeat` derived agentId from the key; `enqueueSystemEvent` ran through `resolveCronSessionKey` with the configured-default agent and was rerouted to that agent's main session under multi-agent deployments where `main` exists but is not the default. The new test exercises the cron-adapter directly via `state.cron.state.deps` with a multi-agent config (`primary` default + `ops` non-default) and a `agent:ops:cron:nightly:run:abc-123` foreign-agent session key, asserting that both call sites resolve the agent target to "ops" rather than falling back to "primary". Refs openclaw#78687. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Refs #52305. Companion to #80214 (cron-run remap at internal enqueue sites) and #71213 (prompt content layer, MERGED). Together with #80214, this PR is intended to close #52305: #78687 covers the external wake/system-event sessionKey surface, while #80214 covers internal cron-run exec/ACP/node-event/watchdog completion routing. The two PRs touch disjoint surfaces and can be reviewed independently.
Adds an optional
sessionKeyto the wake protocol so callers can target a specific session for system events / async-task completion relays instead of always hitting the agent's main session. Without this, the CLI surface foropenclaw system eventwas main-session-only, andWakeParamsSchemaleftsessionKeyopaque, which was the remaining external-callback gap clawsweeper called out on #52305.Changes
WakeParamsSchema(src/gateway/protocol/schema/agent.ts): addsessionKey: Type.Optional(NonEmptyString). Empty / non-string is rejected at the validator boundary.src/gateway/server-methods/cron.ts:179): forwardsessionKeytocontext.cron.wake({...})when provided.src/cron/service-contract.ts,src/cron/service.ts,src/cron/service/ops.ts,src/cron/service/timer.ts): thread optionalsessionKeythroughwake()→wakeNow()→ underlyingwake(state, opts)ops. Whitespace-only keys are treated as omitted.src/gateway/server-cron.ts): when onlysessionKeyis supplied (noagentId),enqueueSystemEventnow derivesagentIdfrom the session key — mirroring whatrequestHeartbeat'sresolveCronWakeTargetalready does. Without this,resolveCronSessionKeywould treat a non-default agent's key as foreign and silently reroute to the default agent's main session, while heartbeat woke the correct session — an asymmetric bug. Caught by codex in P2 review on this branch and fixed before opening.src/cli/system-cli.ts): add--session-key <sessionKey>option toopenclaw system event. Whitespace-only values are treated as omitted to match server-side semantics.docs/cli/system.md): describe the new flag, the safety fall-back (foreign-agent keys → agent's main session), and a callout for thenext-heartbeat+--session-keytiming exception (collapses to immediate targeted wake; addresses [P3] from clawsweeper review).Out of scope
sessionKeyto other RPC surfaces. Onlywakeis in scope.parseAgentSessionKey-style). Defer until there's evidence of misuse —resolveCronSessionKey's existing cross-agent fall-back covers the security concern.Real behavior proof
Behavior or issue addressed: external callers (mobile clients, the
openclaw system eventCLI, plugin SDKs) need a way to target a specific agent session for system-event/wake delivery. The pre-PRWakeParamsSchematyped only{mode, text}withadditionalProperties: true, sosessionKeycould be smuggled in but was opaque and unvalidated. The CLI'ssystem eventhad no way at all to set it. Closing this gap means async-task completion relays from external systems can land in the originating agent's session instead of always hitting the agent's main session.Real environment tested: Local OpenClaw checkout on Linux 6.6.87.2 / WSL2 / Node v25.8.2, branched from current
upstream/mainat95a1c91531(post-v2026.5.6, includes the canvas-plugin refactor that consolidatedapps/macos/Sources/OpenClawProtocol/GatewayModels.swiftintoapps/shared/OpenClawKit/). Gateway runs as systemd user service.Exact steps or command run after this patch: branched from current
upstream/main, applied this PR's schema + CLI + wake-handler + cron-adapter changes, then ran the routing helpers directly vianode --import tsxagainst the patched source to confirm session-key handling in the wake-routing layer for the three key shapes the cron adapter has to deal with (agent-prefixed channel, agent-prefixed cron-run, relative).Evidence after fix:
Confirms (a)
classifySessionKeyShapecorrectly tags agent-prefixed vs relative keys; (b)resolveAgentIdFromSessionKeyextracts the agent id (research) for agent-prefixed shapes and falls back to the literal"main"for relative keys; (c)scopedHeartbeatWakeOptionspasses agent-prefixed keys through to the heartbeat target, but drops the sessionKey (returns{reason}only) for relative keys — because the routing helper alone can't disambiguate which configured agent owns a relative key.That
(c)observation is the asymmetry this PR's cron-adapter change insrc/gateway/server-cron.tscorrects:resolveCronWakeTargetalready calledresolveAgentIdFromSessionKeyand routed wakes to the resolving agent's queue. ButenqueueSystemEventin the same adapter went throughresolveCronSessionKey, which on a multi-agent deployment wheremainis not the configured-default would treat a foreign agent's key as off-target and silently re-route the event to the default agent's main session. Wake fired on agent A; event landed in agent B. After this PR, bothenqueueSystemEventandresolveCronWakeTargetderiveagentIdthe same way (viaparseAgentSessionKey+resolveAgentIdFromSessionKey, then passed to the existingresolveCronAgenthelper), so both sides agree on the target.Integration test (cbded2f):
src/gateway/server-cron.test.tsnow includes a test (derives agentId symmetrically for enqueue and wake when only an agent-prefixed sessionKey is supplied) that constructs the cron-adapter against a multi-agent config (primarydefault +opsnon-default), drivescronDeps.enqueueSystemEventandcronDeps.requestHeartbeatwith the same foreign-agent session key (agent:ops:cron:nightly:run:abc-123), and asserts both observed outputs targetagent:ops:...rather than falling back toprimary. Without this PR's adapter change the assert on the enqueue side would fail because pre-PR'senqueueSystemEventadapter ranresolveCronAgent(undefined)and rerouted toprimary's main queue.Live gateway smoke: built the gateway from the rebased branch and ran it on a separate test instance (
OPENCLAW_HOME=/tmp/oc-test-home, port18790,OPENCLAW_SKIP_CHANNELS=1) with a multi-agent config —primary(default) +ops(non-default), sharedollama-cloud/kimi-k2.5model,agents.defaults.heartbeat.every: "1h"so heartbeats are enabled for every agent (otherwiseisHeartbeatEnabledForAgentonly returns true for the configured-default agent). Then ran twosystem eventcalls back-to-back via the patched CLI:Gateway dispatch log on the test instance:
Two pieces of evidence in each block:
session:agent:ops:…for the foreign-agent call,session:agent:primary:…for the default-agent call. Pre-PR, CASE_A's lane would have readsession:agent:primary:cron:demo:run:case_abecause the enqueue side fell back toresolveCronAgent(undefined)and rerouted to the configured default; the symmetric agentId derivation this PR adds is what makes CASE_A land atops.agents/ops/sessions/…jsonlfor CASE_A vsagents/primary/sessions/…jsonlfor CASE_B. The event was actually filed under the originating agent's on-disk session store, not the configured-default's.Both runs completed successfully (
lane task doneafter ~20–30s) against the liveollama-cloud/kimi-k2.5provider, so this is end-to-end behavior including model dispatch, not just routing.Observed result after fix:
WakeParamsSchemanow typessessionKey: NonEmptyString(optional). Empty string / non-string is rejected at the gateway boundary before reaching the cron service.openclaw system event --session-key <key> --text fooforwards the key in the JSON-RPC payload (verified insrc/cli/system-cli.test.ts); whitespace-only values are treated as omitted.wake({mode: "now", text, sessionKey})andwake({mode: "next-heartbeat", text, sessionKey})both fire a targeted immediate heartbeat when sessionKey is supplied — codex review surfaced (round 5 of this PR's iteration) that anevent-intent wake gets deferred as not-due by the heartbeat-runner and isn't retried, so an immediate wake is the only reliable path. Documented in the wake-fn comment block AND indocs/cli/system.mdas a public timing-exception callout (addresses clawsweeper [P3]).resolveCronAgentresolution for enqueue and wake, so they land in/wake the same target on multi-agent deployments wheremainexists but isn't the configured default. Pre-PR,resolveCronWakeTargetalways derived agentId fromresolveAgentIdFromSessionKeywhileenqueueSystemEventdid not, so wake could targetagent:<resolving>:...while enqueue went toagent:<configured-default>:.... That asymmetry is now fixed.WakeParamsSwift model (apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift) updated to expose the new field. The standaloneapps/macos/Sources/OpenClawProtocol/GatewayModels.swiftwas deleted by upstream's canvas-plugin refactor (commit330ba1fa31); after rebase only the shared one survives.pnpm protocol:checkrunsprotocol:gen+protocol:gen:swift+git diff --exit-code— verified locally clean (exit 0) on the rebased branch with@openclaw/fs-safe@c7ccb99dinstalled.What was not tested:
sessionKeyis a typed field on the JSON-RPC schema, so any caller (mobile client, plugin SDK, channel webhook, native chat, theopenclaw system eventCLI itself) that targets a specific session benefits the same way. The live smoke above exercises the CLI surface; other callers share the same gateway/cron-adapter code path.Test plan
src/cli/system-cli.test.ts):--session-keyis forwarded in the JSON-RPC payload; omitted / whitespace-only values do not appear in the payload.src/gateway/server-methods/cron.validation.test.ts): forwardssessionKeytocontext.cron.wakewhen present, omits when absent, schema rejects empty / non-string values.src/cron/service/wake.test.ts): threadssessionKeyto bothenqueueSystemEventandrequestHeartbeaton bothnowandnext-heartbeatmodes (collapses to targeted-immediate when sessionKey is supplied so the runner doesn't drop the wake as not-due); whitespace-only keys treated as omitted.src/gateway/server-cron.test.ts, commitcbded2f68f): with multi-agent config (defaultprimary+ non-defaultops) and a foreign-agent session key, bothenqueueSystemEventandrequestHeartbeatadapter call sites resolveagentIdtoopssymmetrically. Pre-PR this assert would fail on the enqueue side (rerouted toprimary's main).ollama-cloud/kimi-k2.5):--session-keyflag advertised in CLI help,system event --session-keyaccepted by the wake RPC ({"ok": true}); foreign-agent call lands atsession:agent:ops:…lane with sessionFile underagents/ops/sessions/…, default-agent call atsession:agent:primary:…lane with sessionFile underagents/primary/sessions/…; both agent runs completed against the live provider.wake({mode, text})callers (nosessionKey) continue to default to agent-main session — verified by the omitted-when-absent tests above.pnpm protocol:checkexit 0 locally on the rebased branch — Swift model regen matches the gen output.docs/cli/system.md; the proof block above uses only helpers actually exported fromsrc/routing/session-key.ts.Security Impact
resolveCronSessionKeyis preserved: keys that don't belong to the resolving agent fall back to that agent's main session, so a caller cannot inject events into a different agent's queue.