* fix: make cleanup "keep" persist subagent sessions indefinitely * feat: expose subagent session metadata in sessions list * fix: include status and timing in sessions_list tool * fix: hide injected timestamp prefixes in chat ui * feat: push session list updates over websocket * feat: expose child subagent sessions in subagents list * feat: add admin http endpoint to kill sessions * Emit session.message websocket events for transcript updates * Estimate session costs in sessions list * Add direct session history HTTP and SSE endpoints * Harden dashboard session events and history APIs * Add session lifecycle gateway methods * Add dashboard session API improvements * Add dashboard session model and parent linkage support * fix: tighten dashboard session API metadata * Fix dashboard session cost metadata * Persist accumulated session cost * fix: stop followup queue drain cfg crash * Fix dashboard session create and model metadata * fix: stop guessing session model costs * Gateway: cache OpenRouter pricing for configured models * Gateway: add timeout session status * Fix subagent spawn test config loading * Gateway: preserve operator scopes without device identity * Emit user message transcript events and deduplicate plugin warnings * feat: emit sessions.changed lifecycle event on subagent spawn Adds a session-lifecycle-events module (similar to transcript-events) that emits create events when subagents are spawned. The gateway server.impl.ts listens for these events and broadcasts sessions.changed with reason=create to SSE subscribers, so dashboards can pick up new subagent sessions without polling. * Gateway: allow persistent dashboard orchestrator sessions * fix: preserve operator scopes for token-authenticated backend clients Backend clients (like agent-dashboard) that authenticate with a valid gateway token but don't present a device identity were getting their scopes stripped. The scope-clearing logic ran before checking the device identity decision, so even when evaluateMissingDeviceIdentity returned 'allow' (because roleCanSkipDeviceIdentity passed for token-authed operators), scopes were already cleared. Fix: also check decision.kind before clearing scopes, so token-authenticated operators keep their requested scopes. * Gateway: allow operator-token session kills * Fix stale active subagent status after follow-up runs * Fix dashboard image attachments in sessions send * Fix completed session follow-up status updates * feat: stream session tool events to operator UIs * Add sessions.steer gateway coverage * Persist subagent timing in session store * Fix subagent session transcript event keys * Fix active subagent session status in gateway * bump session label max to 512 * Fix gateway send session reactivation * fix: publish terminal session lifecycle state * feat: change default session reset to effectively never - Change DEFAULT_RESET_MODE from "daily" to "idle" - Change DEFAULT_IDLE_MINUTES from 60 to 0 (0 = disabled/never) - Allow idleMinutes=0 through normalization (don't clamp to 1) - Treat idleMinutes=0 as "no idle expiry" in evaluateSessionFreshness - Default behavior: mode "idle" + idleMinutes 0 = sessions never auto-reset - Update test assertion for new default mode * fix: prep session management followups (#50101) (thanks @clay-datacurve) --------- Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
9.9 KiB
summary, read_when, title
| summary | read_when | title | |
|---|---|---|---|
| Agent session tools for listing sessions, fetching history, and sending cross-session messages |
|
Session Tools |
Session Tools
Goal: small, hard-to-misuse tool set so agents can list sessions, fetch history, and send to another session.
Tool Names
sessions_listsessions_historysessions_sendsessions_spawn
Key Model
- Main direct chat bucket is always the literal key
"main"(resolved to the current agent’s main key). - Group chats use
agent:<agentId>:<channel>:group:<id>oragent:<agentId>:<channel>:channel:<id>(pass the full key). - Cron jobs use
cron:<job.id>. - Hooks use
hook:<uuid>unless explicitly set. - Node sessions use
node-<nodeId>unless explicitly set.
global and unknown are reserved values and are never listed. If session.scope = "global", we alias it to main for all tools so callers never see global.
sessions_list
List sessions as an array of rows.
Parameters:
kinds?: string[]filter: any of"main" | "group" | "cron" | "hook" | "node" | "other"limit?: numbermax rows (default: server default, clamp e.g. 200)activeMinutes?: numberonly sessions updated within N minutesmessageLimit?: number0 = no messages (default 0); >0 = include last N messages
Behavior:
messageLimit > 0fetcheschat.historyper session and includes the last N messages.- Tool results are filtered out in list output; use
sessions_historyfor tool messages. - When running in a sandboxed agent session, session tools default to spawned-only visibility (see below).
Row shape (JSON):
key: session key (string)kind:main | group | cron | hook | node | otherchannel:whatsapp | telegram | discord | signal | imessage | webchat | internal | unknowndisplayName(group display label if available)updatedAt(ms)sessionIdmodel,contextTokens,totalTokensthinkingLevel,verboseLevel,systemSent,abortedLastRunsendPolicy(session override if set)lastChannel,lastTodeliveryContext(normalized{ channel, to, accountId }when available)transcriptPath(best-effort path derived from store dir + sessionId)messages?(only whenmessageLimit > 0)
sessions_history
Fetch transcript for one session.
Parameters:
sessionKey(required; accepts session key orsessionIdfromsessions_list)limit?: numbermax messages (server clamps)includeTools?: boolean(default false)
Behavior:
includeTools=falsefiltersrole: "toolResult"messages.- Returns messages array in the raw transcript format.
- When given a
sessionId, OpenClaw resolves it to the corresponding session key (missing ids error).
Gateway session history and live transcript APIs
Control UI and gateway clients can use the lower level history and live transcript surfaces directly.
HTTP:
GET /sessions/{sessionKey}/history- Query params:
limit,cursor,includeTools=1,follow=1 - Unknown sessions return HTTP
404witherror.type = "not_found" follow=1upgrades the response to an SSE stream of transcript updates for that session
WebSocket:
sessions.subscribesubscribes to all session lifecycle and transcript events visible to the clientsessions.messages.subscribe { key }subscribes only tosession.messageevents for one sessionsessions.messages.unsubscribe { key }removes that targeted transcript subscriptionsession.messagecarries appended transcript messages plus live usage metadata when availablesessions.changedemitsphase: "message"for transcript appends so session lists can refresh counters and previews
sessions_send
Send a message into another session.
Parameters:
sessionKey(required; accepts session key orsessionIdfromsessions_list)message(required)timeoutSeconds?: number(default >0; 0 = fire-and-forget)
Behavior:
timeoutSeconds = 0: enqueue and return{ runId, status: "accepted" }.timeoutSeconds > 0: wait up to N seconds for completion, then return{ runId, status: "ok", reply }.- If wait times out:
{ runId, status: "timeout", error }. Run continues; callsessions_historylater. - If the run fails:
{ runId, status: "error", error }. - Announce delivery runs after the primary run completes and is best-effort;
status: "ok"does not guarantee the announce was delivered. - Waits via gateway
agent.wait(server-side) so reconnects don't drop the wait. - Agent-to-agent message context is injected for the primary run.
- Inter-session messages are persisted with
message.provenance.kind = "inter_session"so transcript readers can distinguish routed agent instructions from external user input. - After the primary run completes, OpenClaw runs a reply-back loop:
- Round 2+ alternates between requester and target agents.
- Reply exactly
REPLY_SKIPto stop the ping‑pong. - Max turns is
session.agentToAgent.maxPingPongTurns(0–5, default 5).
- Once the loop ends, OpenClaw runs the agent‑to‑agent announce step (target agent only):
- Reply exactly
ANNOUNCE_SKIPto stay silent. - Any other reply is sent to the target channel.
- Announce step includes the original request + round‑1 reply + latest ping‑pong reply.
- Reply exactly
Channel Field
- For groups,
channelis the channel recorded on the session entry. - For direct chats,
channelmaps fromlastChannel. - For cron/hook/node,
channelisinternal. - If missing,
channelisunknown.
Security / Send Policy
Policy-based blocking by channel/chat type (not per session id).
{
"session": {
"sendPolicy": {
"rules": [
{
"match": { "channel": "discord", "chatType": "group" },
"action": "deny"
}
],
"default": "allow"
}
}
}
Runtime override (per session entry):
sendPolicy: "allow" | "deny"(unset = inherit config)- Settable via
sessions.patchor owner-only/send on|off|inherit(standalone message).
Enforcement points:
chat.send/agent(gateway)- auto-reply delivery logic
sessions_spawn
Spawn a sub-agent run in an isolated session and announce the result back to the requester chat channel.
Parameters:
task(required)label?(optional; used for logs/UI)agentId?(optional; spawn under another agent id if allowed)model?(optional; overrides the sub-agent model; invalid values error)thinking?(optional; overrides thinking level for the sub-agent run)runTimeoutSeconds?(defaults toagents.defaults.subagents.runTimeoutSecondswhen set, otherwise0; when set, aborts the sub-agent run after N seconds)thread?(default false; request thread-bound routing for this spawn when supported by the channel/plugin)mode?(run|session; defaults torun, but defaults tosessionwhenthread=true;mode="session"requiresthread=true)cleanup?(delete|keep, defaultkeep)sandbox?(inherit|require, defaultinherit;requirerejects spawn unless the target child runtime is sandboxed)attachments?(optional array of inline files; subagent runtime only, ACP rejects). Each entry:{ name, content, encoding?: "utf8" | "base64", mimeType? }. Files are materialized into the child workspace at.openclaw/attachments/<uuid>/. Returns a receipt with sha256 per file.attachAs?(optional;{ mountPath? }hint reserved for future mount implementations)
Allowlist:
agents.list[].subagents.allowAgents: list of agent ids allowed viaagentId(["*"]to allow any). Default: only the requester agent.- Sandbox inheritance guard: if the requester session is sandboxed,
sessions_spawnrejects targets that would run unsandboxed.
Discovery:
- Use
agents_listto discover which agent ids are allowed forsessions_spawn.
Behavior:
- Starts a new
agent:<agentId>:subagent:<uuid>session withdeliver: false. - Sub-agents default to the full tool set minus session tools (configurable via
tools.subagents.tools). - Sub-agents are not allowed to call
sessions_spawn(no sub-agent → sub-agent spawning). - Always non-blocking: returns
{ status: "accepted", runId, childSessionKey }immediately. - With
thread=true, channel plugins can bind delivery/routing to a thread target (Discord support is controlled bysession.threadBindings.*andchannels.discord.threadBindings.*). - After completion, OpenClaw runs a sub-agent announce step and posts the result to the requester chat channel.
- If the assistant final reply is empty, the latest
toolResultfrom sub-agent history is included asResult.
- If the assistant final reply is empty, the latest
- Reply exactly
ANNOUNCE_SKIPduring the announce step to stay silent. - Announce replies are normalized to
Status/Result/Notes;Statuscomes from runtime outcome (not model text). - Sub-agent sessions are auto-archived after
agents.defaults.subagents.archiveAfterMinutes(default: 60). - Announce replies include a stats line (runtime, tokens, sessionKey/sessionId, transcript path, and optional cost).
Sandbox Session Visibility
Session tools can be scoped to reduce cross-session access.
Default behavior:
tools.sessions.visibilitydefaults totree(current session + spawned subagent sessions).- For sandboxed sessions,
agents.defaults.sandbox.sessionToolsVisibilitycan hard-clamp visibility.
Config:
{
tools: {
sessions: {
// "self" | "tree" | "agent" | "all"
// default: "tree"
visibility: "tree",
},
},
agents: {
defaults: {
sandbox: {
// default: "spawned"
sessionToolsVisibility: "spawned", // or "all"
},
},
},
}
Notes:
self: only the current session key.tree: current session + sessions spawned by the current session.agent: any session belonging to the current agent id.all: any session (cross-agent access still requirestools.agentToAgent).- When a session is sandboxed and
sessionToolsVisibility="spawned", OpenClaw clamps visibility totreeeven if you settools.sessions.visibility="all".