Skip to content

Commit eb7f3b7

Browse files
Kaspreclawsweeper[bot]Takhoffman
authored
fix(agent): support explicit CLI session keys (#85121)
Summary: - The PR adds `openclaw agent --session-key`, normalizes explicit session keys through Gateway and embedded agent execution, and updates docs, tests, and changelog. - Reproducibility: yes. Current main's `openclaw agent` registration and gateway CLI option type lack `--sessi ... Gateway agent protocol already accepts `sessionKey`; this is source-reproducible without executing the CLI. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(agent): support explicit CLI session keys Validation: - ClawSweeper review passed for head 2c76dd3. - Required merge gates passed before the squash merge. Prepared head SHA: 2c76dd3 Review: #85121 (comment) Co-authored-by: Kaspre <kaspre@gmail.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
1 parent a4c81c6 commit eb7f3b7

12 files changed

Lines changed: 627 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
2323

2424
### Fixes
2525

26+
- CLI/agents: allow `openclaw agent --session-key` to target explicit session keys, including agent-scoped legacy keys. (#85121) Thanks @Kaspre.
2627
- Agents/subagents: surface blocked child-run completions as errors instead of successful subagent finishes. (#80886) Thanks @TurboTheTurtle.
2728
- Agents/Pi: treat accepted embedded `sessions_spawn` child-session handoffs as terminal progress so parent turns no longer report false non-deliverable failures. (#85054) Thanks @samzong.
2829
- WhatsApp: update Baileys to `7.0.0-rc13` and drop the obsolete logger type patch.

docs/cli/agent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Use `--agent <id>` to target a configured agent directly.
1313
Pass at least one session selector:
1414

1515
- `--to <dest>`
16+
- `--session-key <key>`
1617
- `--session-id <id>`
1718
- `--agent <id>`
1819

@@ -24,6 +25,7 @@ Related:
2425

2526
- `-m, --message <text>`: required message body
2627
- `-t, --to <dest>`: recipient used to derive the session key
28+
- `--session-key <key>`: explicit session key to use for routing
2729
- `--session-id <id>`: explicit session id
2830
- `--agent <id>`: agent id; overrides routing bindings
2931
- `--model <id>`: model override for this run (`provider/model` or model id)
@@ -44,6 +46,8 @@ Related:
4446
openclaw agent --to +15555550123 --message "status update" --deliver
4547
openclaw agent --agent ops --message "Summarize logs"
4648
openclaw agent --agent ops --model openai/gpt-5.4 --message "Summarize logs"
49+
openclaw agent --session-key agent:ops:incident-42 --message "Summarize status"
50+
openclaw agent --agent ops --session-key incident-42 --message "Summarize status"
4751
openclaw agent --session-id 1234 --message "Summarize inbox" --thinking medium
4852
openclaw agent --to +15555550123 --message "Trace logs" --verbose on --json
4953
openclaw agent --agent ops --message "Generate report" --deliver --reply-channel slack --reply-to "#reports"
@@ -57,6 +61,7 @@ openclaw agent --agent ops --message "Run locally" --local
5761
- `--local` and embedded fallback runs are treated as one-shot runs. Bundled MCP loopback resources and warm Claude stdio sessions opened for that local process are retired after the reply, so scripted invocations do not keep local child processes alive.
5862
- Gateway-backed runs leave Gateway-owned MCP loopback resources under the running Gateway process; older clients may still send the historical cleanup flag, but the Gateway accepts it as a compatibility no-op.
5963
- `--channel`, `--reply-channel`, and `--reply-account` affect reply delivery, not session routing.
64+
- `--session-key` selects an explicit session key. Agent-prefixed keys must use `agent:<agent-id>:<session-key>`, and `--agent` must match the key's agent id when both are provided. Bare non-sentinel keys are scoped to `--agent` when supplied, or to the configured default agent otherwise; for example, `--agent ops --session-key incident-42` routes to `agent:ops:incident-42`. Literal `global` and `unknown` remain unscoped only when no `--agent` is supplied; in that case, embedded fallback and store ownership use the configured default agent.
6065
- `--json` keeps stdout reserved for the JSON response. Gateway, plugin, and embedded-fallback diagnostics are routed to stderr so scripts can parse stdout directly.
6166
- Embedded fallback JSON includes `meta.transport: "embedded"` and `meta.fallbackFrom: "gateway"` so scripts can distinguish fallback runs from Gateway runs.
6267
- If the Gateway accepts an agent run but the CLI times out waiting for the final reply, embedded fallback uses a fresh explicit `gateway-fallback-*` session/run id and reports `meta.fallbackReason: "gateway_timeout"` plus the fallback session fields. This avoids racing the Gateway-owned transcript lock or silently replacing the original routed conversation session.

docs/tools/agent-send.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ programmatic delivery.
1515
<Steps>
1616
<Step title="Run a simple agent turn">
1717
```bash
18-
openclaw agent --message "What is the weather today?"
18+
openclaw agent --agent main --message "What is the weather today?"
1919
```
2020

2121
This sends the message through the Gateway and prints the reply.
@@ -32,6 +32,9 @@ programmatic delivery.
3232

3333
# Reuse an existing session
3434
openclaw agent --session-id abc123 --message "Continue the task"
35+
36+
# Target an exact session key
37+
openclaw agent --session-key agent:ops:incident-42 --message "Summarize status"
3538
```
3639

3740
</Step>
@@ -55,6 +58,7 @@ programmatic delivery.
5558
| ----------------------------- | ----------------------------------------------------------- |
5659
| `--message \<text\>` | Message to send (required) |
5760
| `--to \<dest\>` | Derive session key from a target (phone, chat id) |
61+
| `--session-key \<key\>` | Use an explicit session key |
5862
| `--agent \<id\>` | Target a configured agent (uses its `main` session) |
5963
| `--session-id \<id\>` | Reuse an existing session by id |
6064
| `--local` | Force local embedded runtime (skip Gateway) |
@@ -75,6 +79,14 @@ programmatic delivery.
7579
- If the Gateway is unreachable, the CLI **falls back** to the local embedded run.
7680
- Session selection: `--to` derives the session key (group/channel targets
7781
preserve isolation; direct chats collapse to `main`).
82+
- `--session-key` selects an explicit key. Agent-prefixed keys must use
83+
`agent:<agent-id>:<session-key>`, and `--agent` must match that agent id when
84+
both are supplied. Bare non-sentinel keys are scoped to `--agent` when
85+
supplied; for example, `--agent ops --session-key incident-42` routes to
86+
`agent:ops:incident-42`. Without `--agent`, bare non-sentinel keys are scoped
87+
to the configured default agent. Literal `global` and `unknown` remain
88+
unscoped only when no `--agent` is supplied; in that case, embedded fallback
89+
and store ownership use the configured default agent.
7890
- Thinking and verbose flags persist into the session store.
7991
- Output: plain text by default, or `--json` for structured payload + metadata.
8092
- With `--json --deliver`, the JSON includes delivery status for sent,
@@ -90,6 +102,12 @@ openclaw agent --to +15555550123 --message "Trace logs" --verbose on --json
90102
# Turn with thinking level
91103
openclaw agent --session-id 1234 --message "Summarize inbox" --thinking medium
92104

105+
# Exact session key
106+
openclaw agent --session-key agent:ops:incident-42 --message "Summarize status"
107+
108+
# Legacy key scoped to an agent
109+
openclaw agent --agent ops --session-key incident-42 --message "Summarize status"
110+
93111
# Deliver to a different channel than the session
94112
openclaw agent --agent ops --message "Alert" --deliver --reply-channel telegram --reply-to "@admin"
95113
```

src/agents/agent-command.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ import { buildOutboundSessionContext } from "../infra/outbound/session-context.j
2222
import { createSubsystemLogger } from "../logging/subsystem.js";
2323
import { loadManifestMetadataSnapshot } from "../plugins/manifest-contract-eligibility.js";
2424
import {
25+
classifySessionKeyShape,
26+
isUnscopedSessionKeySentinel,
2527
isSubagentSessionKey,
2628
normalizeAgentId,
2729
resolveAgentIdFromSessionKey,
30+
scopeLegacySessionKeyToAgent,
2831
} from "../routing/session-key.js";
2932
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
3033
import { applyVerboseOverride } from "../sessions/level-overrides.js";
@@ -48,6 +51,7 @@ import {
4851
markAutoFallbackPrimaryProbe,
4952
resolveAutoFallbackPrimaryProbe,
5053
resolveAgentDir,
54+
resolveDefaultAgentId,
5155
resolveEffectiveModelFallbacks,
5256
resolveSessionAgentId,
5357
resolveAgentSkillsFilter,
@@ -322,8 +326,11 @@ async function prepareAgentCommandExecution(opts: AgentCommandOpts, runtime: Run
322326
if (!message.trim()) {
323327
throw new Error("Message (--message) is required");
324328
}
325-
if (!opts.to && !opts.sessionId && !opts.sessionKey && !opts.agentId) {
326-
throw new Error("Pass --to <E.164>, --session-id, or --agent to choose a session");
329+
const rawExplicitSessionKey = opts.sessionKey?.trim();
330+
if (!opts.to && !opts.sessionId && !rawExplicitSessionKey && !opts.agentId) {
331+
throw new Error(
332+
"Pass --to <E.164>, --session-key, --session-id, or --agent to choose a session",
333+
);
327334
}
328335

329336
const { cfg } = await resolveAgentRuntimeConfig(runtime, {
@@ -346,8 +353,28 @@ async function prepareAgentCommandExecution(opts: AgentCommandOpts, runtime: Run
346353
);
347354
}
348355
}
349-
if (agentIdOverride && opts.sessionKey) {
350-
const sessionAgentId = resolveAgentIdFromSessionKey(opts.sessionKey);
356+
const shouldScopeDefaultAgentKey =
357+
rawExplicitSessionKey &&
358+
!agentIdOverride &&
359+
classifySessionKeyShape(rawExplicitSessionKey) === "legacy_or_alias" &&
360+
!isUnscopedSessionKeySentinel(rawExplicitSessionKey);
361+
const explicitSessionKey = scopeLegacySessionKeyToAgent({
362+
agentId:
363+
agentIdOverride ?? (shouldScopeDefaultAgentKey ? resolveDefaultAgentId(cfg) : undefined),
364+
sessionKey: rawExplicitSessionKey,
365+
mainKey: cfg.session?.mainKey,
366+
});
367+
if (explicitSessionKey && classifySessionKeyShape(explicitSessionKey) === "malformed_agent") {
368+
throw new Error(
369+
`Invalid --session-key "${explicitSessionKey}". Agent-prefixed session keys must use agent:<agent-id>:<session-key>.`,
370+
);
371+
}
372+
if (
373+
agentIdOverride &&
374+
explicitSessionKey &&
375+
classifySessionKeyShape(explicitSessionKey) === "agent"
376+
) {
377+
const sessionAgentId = resolveAgentIdFromSessionKey(explicitSessionKey);
351378
if (sessionAgentId !== agentIdOverride) {
352379
throw new Error(
353380
`Agent id "${agentIdOverrideRaw}" does not match session key agent "${sessionAgentId}".`,
@@ -381,7 +408,7 @@ async function prepareAgentCommandExecution(opts: AgentCommandOpts, runtime: Run
381408
cfg,
382409
to: opts.to,
383410
sessionId: opts.sessionId,
384-
sessionKey: opts.sessionKey,
411+
sessionKey: explicitSessionKey,
385412
agentId: agentIdOverride,
386413
});
387414

@@ -398,7 +425,7 @@ async function prepareAgentCommandExecution(opts: AgentCommandOpts, runtime: Run
398425
const sessionAgentId =
399426
agentIdOverride ??
400427
resolveSessionAgentId({
401-
sessionKey: sessionKey ?? opts.sessionKey?.trim(),
428+
sessionKey: sessionKey ?? explicitSessionKey,
402429
config: cfg,
403430
});
404431
const outboundSession = buildOutboundSessionContext({

src/agents/command/session.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
2424
import {
2525
buildAgentMainSessionKey,
2626
DEFAULT_AGENT_ID,
27+
isUnscopedSessionKeySentinel,
2728
normalizeAgentId,
2829
normalizeMainKey,
2930
} from "../../routing/session-key.js";
@@ -218,7 +219,9 @@ export function resolveSessionKeyForRequest(opts: {
218219
})
219220
: undefined);
220221
const storeAgentId = explicitSessionKey
221-
? resolveAgentIdFromSessionKey(explicitSessionKey)
222+
? isUnscopedSessionKeySentinel(explicitSessionKey)
223+
? (requestedAgentId ?? defaultAgentId)
224+
: resolveAgentIdFromSessionKey(explicitSessionKey)
222225
: (requestedAgentId ?? defaultAgentId);
223226
const storePath = resolveStorePath(sessionCfg?.store, {
224227
agentId: storeAgentId,

src/cli/program/register.agent.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,16 @@ describe("registerAgentCommands", () => {
134134
expect(deps).toEqual({ deps: true });
135135
});
136136

137+
it("forwards an explicit session key to the agent command", async () => {
138+
await runCli(["agent", "--message", "hi", "--session-key", "agent:ops:incident-42"]);
139+
140+
const [options, callRuntime, deps] = commandCall(agentCliCommandMock);
141+
expect((options as { message?: string }).message).toBe("hi");
142+
expect((options as { sessionKey?: string }).sessionKey).toBe("agent:ops:incident-42");
143+
expect(callRuntime).toBe(runtime);
144+
expect(deps).toEqual({ deps: true });
145+
});
146+
137147
it("runs agents add and computes hasFlags based on explicit options", async () => {
138148
await runCli(["agents", "add", "alpha"]);
139149
const [alphaOptions, alphaRuntime, alphaFlags] = commandCall(agentsAddCommandMock, 0);

src/cli/program/register.agent.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export function registerAgentCommands(
6868
.description("Run an agent turn via the Gateway (use --local for embedded)")
6969
.requiredOption("-m, --message <text>", "Message body for the agent")
7070
.option("-t, --to <number>", "Recipient number in E.164 used to derive the session key")
71+
.option("--session-key <key>", "Explicit session key (agent:<id>:<key>, or scoped to --agent)")
7172
.option("--session-id <id>", "Use an explicit session id")
7273
.option("--agent <id>", "Agent id (overrides routing bindings)")
7374
.option("--model <id>", "Model override for this run (provider/model or model id)")
@@ -102,6 +103,10 @@ ${theme.heading("Examples:")}
102103
${formatHelpExamples([
103104
['openclaw agent --to +15555550123 --message "status update"', "Start a new session."],
104105
['openclaw agent --agent ops --message "Summarize logs"', "Use a specific agent."],
106+
[
107+
'openclaw agent --session-key agent:ops:incident-42 --message "Summarize status"',
108+
"Target an exact session key.",
109+
],
105110
[
106111
'openclaw agent --session-id 1234 --message "Summarize inbox" --thinking medium',
107112
"Target a session with explicit thinking level.",

0 commit comments

Comments
 (0)