feat(sessions): let sessions_send register explicit state-change watchers (#104815)

* feat(sessions): let sessions_send register explicit state-change watchers

* feat(sessions): terse self-contained notices, immediate stale-state wakes, session-state docs page

* chore: regenerate prompt snapshots after rebase
This commit is contained in:
Peter Steinberger
2026-07-12 18:10:51 +01:00
committed by GitHub
parent 7c62ae1000
commit 3bde3cd017
14 changed files with 347 additions and 30 deletions

View File

@@ -0,0 +1,106 @@
---
summary: "Durable session state signal log: state versions, watchers, stale-state notices, and reconciliation"
read_when:
- You want agents to notice when humans or other agents change a session behind their back
- You are debugging state-change notices, watch cursors, or session_status changesSince
- You want to understand how parent agents stay synchronized with child sessions
title: "Session state awareness"
sidebarTitle: "Session state awareness"
---
When several sessions work on the same problem — a manager delegating to children, a human jumping directly into a worker session, two agents coordinating over [`sessions_send`](/concepts/session-tool) — each session builds assumptions about the others. Those assumptions go stale the moment another actor intervenes. Session state awareness is the machinery that detects the intervention, tells the affected session once, and gives it a cheap way to catch up before acting.
Three pieces work together:
1. A **durable signal log** records selected state changes per session.
2. **Watchers** hold per-target cursors and receive one coalesced stale-state notice.
3. **Reconciliation** pulls the exact delta via `session_status` with `changesSince`.
## The signal log
OpenClaw appends a typed event to the shared state database (`session_state_events`) when a watched session materially changes. Events carry metadata and a one-line summary — never message content.
| Kind | Recorded when | Notifies watchers |
| ---------------------- | -------------------------------------------------------- | ----------------- |
| `human_direct_message` | A human sends a turn directly to a watched session | Yes |
| `goal_changed` | The session's goal state is created, updated, or cleared | Yes |
| `child_spawned` | A sub-agent or ACP child session is created | No (seeds cursor) |
| `run_completed` | A child run ends successfully | No (log only) |
| `run_failed` | A child run fails, times out, or is cancelled | No (log only) |
| `compacted` | The session's history is compacted | No (log only) |
Each event names its actor (`human`, `agent`, or `system`). Cancelled and timed-out child runs are recorded as failures with the precise outcome (`cancelled`, `timeout`, or `error`) preserved in the event payload.
A session's **state version** is simply the highest sequence number in its log, tracked in a durable per-session head that survives pruning. `sessions_list` rows include `stateVersion` when a session has logged changes; `session_status` always reports it.
Log-only kinds exist for reconciliation history, not notification: ordinary child-run completion delivery stays owned by [sub-agent announcements](/tools/subagents), and the signal log never duplicates it.
## Watchers
A watcher is a session that holds a cursor (`session_watch_cursors`) on a target. Cursors come from two places:
- **Implicit (spawn edges).** When a session spawns a sub-agent or ACP child, the parent's cursor is seeded automatically at the child's spawn version. Parents never subscribe manually.
- **Explicit (`sessions_send watch: true`).** Any coordinator can watch a non-spawned target: pass `watch: true` on `sessions_send`, and after the send dispatches successfully the sender is registered as a watcher of the session that actually received the message. Registration starts at the target's current state version — prior history never produces notices. The tool result reports `watched: true|false` when the parameter was set.
Watcher identity must be an agent-qualified session key. Under `session.scope="global"` the shared `global` key is ambiguous across agents, so such sessions get the durable log and `changesSince` but no proactive notices.
Watches clean themselves up: cursor rows expire with signal-log retention, are removed when the watcher session resets, and are deleted with either session. There is no unwatch verb in v1.
## Notices: one, not many
When a notify-eligible event lands and a watcher's cursor is behind, the watcher receives one system notice on its next turn:
```
Session "agent:main:subagent:child" changed (other actor). Reconcile before acting: session_status sessionKey "agent:main:subagent:child" changesSince 12.
```
Main-session watchers are also woken immediately via a heartbeat wake; nested sub-agent watchers get the notice on their next turn.
The protocol is deliberately anti-spam:
- **One pending notice per watcher/target pair.** The notice text is byte-stable while pending and the system-event queue dedupes on it, so twenty rapid changes to the same target still produce a single line in the watcher's prompt.
- **Frozen watermark.** The cursor freezes its notified position when a notice is queued. Further material events advance only the material watermark; they do not re-notify.
- **Acknowledge on drain, reopen only for interleaved work.** When the watcher's turn consumes the notice, the cursor advances. If more material events arrived between queueing and draining, exactly one fresh notice is opened for the remainder.
- **Self-suppression.** A watcher never gets notified about events it caused itself.
- **Restart recovery.** Pending notices live in an in-memory queue; a startup sweep re-materializes them from durable cursors after a gateway restart.
## Reconciling
The notice tells the watcher exactly what to do. `session_status` with `changesSince: <version>` returns the typed events after that version (up to 200), without advancing any cursors:
```json
{
"stateVersion": 19,
"stateChanges": {
"events": [
{
"sequence": 14,
"kind": "human_direct_message",
"actorType": "human",
"summary": "human message via telegram"
},
{ "sequence": 19, "kind": "goal_changed", "actorType": "human", "summary": "goal updated" }
],
"historyGap": false
}
}
```
`historyGap: true` means the requested version predates retained history — refresh the whole session state (`sessions_history`, `session_status`) instead of treating the response as an exact delta. The gap signal is exact: it comes from a per-session pruned watermark, not inferred from sequence arithmetic.
## Storage and limits
History lives in the shared state database, bounded to 30 days and 50,000 rows; per-session heads stay monotonic after pruning. Recording is best-effort — a failed append is logged and never fails the originating turn — so `stateVersion` is a signal-log head, not a transactional change-data-capture version.
Current limits:
- Notice delivery assumes one gateway process owns the shared state database. Multiple gateways share the durable log and `changesSince`, but v1 does not push notices across processes.
- Compaction events cover the embedded runtime's compaction owners; native-harness-only compaction is not fully logged.
- Cancelled-outcome payload detail is currently produced by ACP child runs; native sub-agent cancellations surface as generic failures.
## Related
- [Session tools](/concepts/session-tool) — `sessions_send`, `session_status`, `sessions_list`
- [Sub-agents](/tools/subagents) — spawn edges and completion announcements
- [Heartbeat](/gateway/heartbeat) — how queued notices wake main sessions
- [Session management](/concepts/session) — session keys, scopes, lifecycle

View File

@@ -71,6 +71,8 @@ Messages and A2A follow-up replies are marked as inter-session data in the recei
After the target responds, OpenClaw can run a **reply-back loop** where the agents alternate messages (up to `session.agentToAgent.maxPingPongTurns`, range 0-20, default 5). The target agent can reply `REPLY_SKIP` to stop early.
Pass `watch: true` to also register the sender as a state-change watcher of the target: when another actor later sends the target a direct human message or changes its goal, the sender receives a system notice pointing at `session_status` `changesSince`. Registration happens after successful dispatch, targets the session that actually received the message, and starts at its current state version, so only later changes produce notices. The result reports `watched: true` when registration succeeded. See [Session state awareness](/concepts/session-state).
## Status and orchestration helpers
`session_status` is the lightweight `/status`-equivalent tool for the current or another visible session. It reports usage, time, model/runtime state, and linked background-task context when present. Like `/status`, it can backfill sparse token/cache counters from the latest transcript usage entry, and `model=default` clears a per-session override. Use `sessionKey="current"` for the caller's current session; visible client labels such as `openclaw-tui` are not session keys.
@@ -83,13 +85,9 @@ When route metadata is available, `session_status` also includes a visible `Rout
## Session state changes
OpenClaw keeps a best-effort signal log for selected session state changes: direct human messages to child sessions, child-run completion or failure, child creation, goal changes, and compaction. Cancelled and timed-out child runs are recorded as failures, with the specific outcome (`cancelled`, `timeout`, or `error`) preserved in the event payload. The log contains metadata and one-line summaries, never message content. Its `stateVersion` is the session's signal-log head, not a transactional change-data-capture version; the session-store mutation and signal append use separate storage, so a failed append is logged without failing the originating turn.
OpenClaw keeps a durable signal log of material session state changes (direct human messages to watched sessions, child-run outcomes, goal changes, compaction). `sessions_list` rows and `session_status` expose the session's `stateVersion`, and `session_status` accepts `changesSince: <version>` to return the typed events after that version, with exact `historyGap` signaling when the requested version predates retained history. Watchers — spawn parents automatically, `sessions_send watch: true` explicitly — receive one coalesced stale-state notice when another actor changes a watched session.
`sessions_list` includes `stateVersion` on rows with logged changes. `session_status` always returns `stateVersion` in structured details. Pass `changesSince: <previousStateVersion>` to retrieve up to 200 retained events after that version; this read does not acknowledge or advance parent notification cursors. A `historyGap: true` result means the requested version predates retained history, so refresh the whole session state instead of treating the response as an exact delta.
When another actor sends a direct human turn to a watched child or changes its goal, the parent receives a system notice telling it to call `session_status` with its last-seen version. Main-session parents are proactively woken. Nested sub-agent parents receive the notice on their next turn because heartbeat routing cannot target their queue directly. Completion announcements remain the owner for ordinary child-run completion delivery.
History is bounded to 30 days and 50,000 rows, while per-session heads remain monotonic after pruning. Notice delivery uses the gateway's in-memory system-event queue and assumes one gateway process owns delivery for the shared state database. Multiple gateways still share the durable log and `changesSince` reconciliation surface, but v1 does not push notices across processes. Parent notices require an agent-qualified parent session key; under `session.scope="global"` the shared `global` key is ambiguous across agents, so those parents get the durable log and `changesSince` but no proactive notices in v1.
See [Session state awareness](/concepts/session-state) for the full model: event kinds, watcher registration, the anti-spam notice protocol, reconciliation flow, and current limits.
`sessions_yield` intentionally ends the current turn so the next message can be the follow-up event you are waiting for. Use it after spawning sub-agents when you want completion results to arrive as the next message instead of building poll loops.

View File

@@ -1219,6 +1219,7 @@
"concepts/session-search",
"concepts/channel-docking",
"concepts/session-pruning",
"concepts/session-state",
"concepts/session-tool",
{
"group": "Memory",

View File

@@ -2868,6 +2868,17 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
- H2: Index lifecycle
- H2: Session search vs. memory search
## concepts/session-state.md
- Route: /concepts/session-state
- Headings:
- H2: The signal log
- H2: Watchers
- H2: Notices: one, not many
- H2: Reconciling
- H2: Storage and limits
- H2: Related
## concepts/session-tool.md
- Route: /concepts/session-tool

View File

@@ -43,6 +43,7 @@ export function describeSessionsSendTool(): string {
return [
"Message visible session by sessionKey/label, or configured agent by agentId; sessionKey wins redundant label.",
"Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available.",
"watch:true: notice arrives when others later change target session.",
].join(" ");
}

View File

@@ -23,6 +23,7 @@ import {
import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js";
import { isCronRunSessionKey, parseAgentSessionKey } from "../../sessions/session-key-utils.js";
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
import { registerSessionStateWatch } from "../../sessions/session-state-events.js";
import { stripFormattedReasoningMessage } from "../../shared/text/formatted-reasoning-message.js";
import {
type GatewayMessageChannel,
@@ -66,6 +67,7 @@ const SessionsSendToolSchema = Type.Object({
agentId: Type.Optional(Type.String({ minLength: 1, maxLength: 64 })),
message: Type.String(),
timeoutSeconds: Type.Optional(Type.Integer({ minimum: 0 })),
watch: Type.Optional(Type.Boolean()),
});
type GatewayCaller = typeof callGateway;
@@ -559,6 +561,20 @@ export function createSessionsSendTool(opts?: {
const requesterChannel = opts?.agentChannel;
const sameSessionA2A = requesterSessionKey === resolvedKey;
const isIsolatedCronRequester = isCronRunSessionKey(requesterSessionKey);
// Watch registration follows successful dispatch: a failed send must not leave
// a hidden watch, and cron run-scoped sends can fall back to the durable parent
// session, which is the key that receives future state changes.
const watchRequested = params.watch === true;
const registerWatchIfRequested = (targetSessionKey: string) => {
const watched =
watchRequested && requesterSessionKey && requesterSessionKey !== targetSessionKey
? registerSessionStateWatch({
watcherSessionKey: requesterSessionKey,
targetSessionKey,
})
: false;
return watchRequested ? { watched } : {};
};
const fallbackA2ASessionKey =
timeoutSeconds === 0 && isIsolatedCronRequester
? resolveCronRunScopedFallbackSessionKey(displayKey)
@@ -705,6 +721,7 @@ export function createSessionsSendTool(opts?: {
return start.result;
}
runId = start.runId;
const watchField = registerWatchIfRequested(start.a2aSessionKey ?? resolvedKey);
if (!start.activeRunQueue) {
startA2AFlow(undefined, runId, start.a2aSessionKey, start.a2aDisplayKey, true);
}
@@ -713,6 +730,7 @@ export function createSessionsSendTool(opts?: {
status: "accepted",
sessionKey: displayKey,
delivery,
...watchField,
});
}
@@ -727,6 +745,7 @@ export function createSessionsSendTool(opts?: {
return start.result;
}
runId = start.runId;
const watchField = registerWatchIfRequested(resolvedKey);
const result = await waitForAgentRunAndReadUpdatedAssistantReply({
runId,
sessionKey: resolvedKey,
@@ -746,6 +765,7 @@ export function createSessionsSendTool(opts?: {
sentBeforeError: true,
sessionKey: displayKey,
delivery,
...watchField,
});
}
if (!isTerminalAgentWaitTimeout(result)) {
@@ -755,6 +775,7 @@ export function createSessionsSendTool(opts?: {
status: "accepted",
sessionKey: displayKey,
delivery,
...watchField,
});
}
return jsonResult({
@@ -763,6 +784,7 @@ export function createSessionsSendTool(opts?: {
error: result.error,
sentBeforeError: true,
sessionKey: displayKey,
...watchField,
});
}
if (result.status === "error") {
@@ -772,6 +794,7 @@ export function createSessionsSendTool(opts?: {
error: result.error ?? "agent error",
sentBeforeError: true,
sessionKey: displayKey,
...watchField,
});
}
const reply = result.replyText;
@@ -783,6 +806,7 @@ export function createSessionsSendTool(opts?: {
reply,
sessionKey: displayKey,
delivery,
...watchField,
});
},
};

View File

@@ -26,9 +26,11 @@ import {
pruneSessionStateEvents,
recordSessionCompacted,
recordSessionGoalChanged,
recordSessionHumanDirectMessage,
recordSessionStateEvent,
recordSubagentSpawned,
recordSubagentTerminalState,
registerSessionStateWatch,
sessionStateEventStoreLimits,
sweepSessionStateWatchNotices,
} from "./session-state-events.js";
@@ -205,7 +207,13 @@ describe("session state events", () => {
recordSessionStateEvent(eventInput(), database);
await vi.advanceTimersByTimeAsync(300);
expect(wakes).toHaveBeenCalledWith(
expect.objectContaining({ source: "session-state", sessionKey: watcher }),
// intent "immediate" is load-bearing: event-intent wakes defer on heartbeat
// dueness and would sit on the notice until the next scheduled tick.
expect.objectContaining({
source: "session-state",
sessionKey: watcher,
intent: "immediate",
}),
);
});
@@ -463,6 +471,73 @@ describe("session state events", () => {
});
});
it("registers explicit watchers who get notices only for later changes", () => {
const database = createDatabaseOptions();
const preRegistration = recordSessionStateEvent(
eventInput({ watcherSessionKeys: [] }),
database,
)!;
expect(registerSessionStateWatch({ watcherSessionKey: child, targetSessionKey: child })).toBe(
false,
);
expect(
registerSessionStateWatch({ watcherSessionKey: "global", targetSessionKey: child }),
).toBe(false);
expect(
registerSessionStateWatch({ watcherSessionKey: watcher, targetSessionKey: child }, database),
).toBe(true);
expect(peekSystemEventEntries(watcher)).toHaveLength(0);
expect(readCursor(database)).toMatchObject({ last_seen_sequence: preRegistration.sequence });
const afterRegistration = recordSessionStateEvent(
eventInput({ watcherSessionKeys: [] }),
database,
)!;
expect(peekSystemEventEntries(watcher)).toHaveLength(1);
expect(peekSystemEventEntries(watcher)[0]?.text).toContain(
`changesSince ${preRegistration.sequence}`,
);
// Re-registering must keep the pending-notice cursor intact.
expect(
registerSessionStateWatch({ watcherSessionKey: watcher, targetSessionKey: child }, database),
).toBe(true);
expect(readCursor(database)).toEqual({
last_seen_sequence: preRegistration.sequence,
notified_sequence: afterRegistration.sequence,
material_sequence: afterRegistration.sequence,
});
});
it("gates unparented human turns on registered watchers", () => {
const database = createDatabaseOptions();
const entry = { sessionId: "session-child", updatedAt: Date.now() };
recordSessionHumanDirectMessage({
sessionKey: child,
entry,
agentId: "main",
actor: { actorType: "human" },
channel: "webchat",
});
expect(listSessionStateEventsSince(child, "main", 0, 200, database).events).toHaveLength(0);
registerSessionStateWatch({ watcherSessionKey: watcher, targetSessionKey: child }, database);
recordSessionHumanDirectMessage({
sessionKey: child,
entry,
agentId: "main",
actor: { actorType: "human" },
channel: "webchat",
});
const events = listSessionStateEventsSince(child, "main", 0, 200, database).events;
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({ kind: "human_direct_message" });
expect(peekSystemEventEntries(watcher)).toHaveLength(1);
});
it("projects spawn, terminal, goal, and compaction producer helpers", () => {
const database = createDatabaseOptions();
recordSubagentSpawned({

View File

@@ -162,8 +162,11 @@ export function decodeSessionStateNoticeContextKey(contextKey: string): string |
return Buffer.from(encoded, "hex").toString("utf8");
}
// Terse on purpose: this line lands in model prompts, possibly repeatedly across
// turns. Text must stay byte-stable per frozen watermark so queue dedupe holds,
// and the reconciliation call must be self-contained (explicit target sessionKey).
function sessionStateNoticeText(targetSessionKey: string, lastSeenSequence: number): string {
return `Another actor has interacted with child session "${targetSessionKey}" since you last synced. Your assumptions about that session may be stale. Call session_status with sessionKey "${targetSessionKey}" and changesSince ${lastSeenSequence} before acting on it.`;
return `Session "${targetSessionKey}" changed (other actor). Reconcile before acting: session_status sessionKey "${targetSessionKey}" changesSince ${lastSeenSequence}.`;
}
function shouldWakeWatcher(watcherSessionKey: string): boolean {
@@ -192,9 +195,12 @@ function enqueueSessionStateNotice(params: {
if (!shouldWakeWatcher(params.watcherSessionKey)) {
return;
}
// intent "immediate": event-intent wakes defer on heartbeat dueness, which would
// delay stale-state notices by up to the whole heartbeat interval. Task/cron
// wake-now paths use the same class; the flood guard remains the backstop.
requestHeartbeat({
source: "session-state",
intent: "event",
intent: "immediate",
reason: `session-state:${params.targetSessionKey}`,
sessionKey: params.watcherSessionKey,
});
@@ -360,9 +366,21 @@ export function recordSessionStateEvent(
),
);
const watcherSessionKeys = [...new Set(input.watcherSessionKeys ?? [])].filter(
(key) => Boolean(key) && isNotifiableWatcherKey(key),
);
// Explicit watch registrations (registerSessionStateWatch) live as cursor rows;
// union them with producer-passed watchers so sessions_send coordinators get
// notices without every producer knowing about registration.
const registeredWatcherKeys = NOTIFY_BY_KIND[input.kind]
? executeSqliteQuerySync(
db,
getSessionStateKysely(db)
.selectFrom("session_watch_cursors")
.select("watcher_session_key")
.where("target_session_key", "=", input.sessionKey),
).rows.map((row) => row.watcher_session_key)
: [];
const watcherSessionKeys = [
...new Set([...(input.watcherSessionKeys ?? []), ...registeredWatcherKeys]),
].filter((key) => Boolean(key) && isNotifiableWatcherKey(key));
for (const watcherSessionKey of watcherSessionKeys) {
if (input.kind === "child_spawned") {
upsertSeedCursor({
@@ -813,7 +831,76 @@ export function recordSessionGoalChanged(params: {
});
}
/** Record a direct human turn only when the target has an implicit parent watcher. */
/** True when any seeded or explicitly registered watcher cursor targets this session. */
function hasSessionStateWatchers(
targetSessionKey: string,
options: OpenClawStateDatabaseOptions = {},
): boolean {
try {
const { db } = openOpenClawStateDatabase(options);
const row = executeSqliteQueryTakeFirstSync(
db,
getSessionStateKysely(db)
.selectFrom("session_watch_cursors")
.select("watcher_session_key")
.where("target_session_key", "=", targetSessionKey)
.limit(1),
);
return row !== undefined;
} catch (error) {
// Best-effort log: enrichment reads must never fail core session tools.
log.warn(`failed to probe session state watchers: ${String(error)}`);
return false;
}
}
/** Register an explicit watcher (e.g. a sessions_send coordinator) for a target session. */
export function registerSessionStateWatch(
params: { watcherSessionKey: string; targetSessionKey: string; targetAgentId?: string },
options: OpenClawStateDatabaseOptions & { now?: number } = {},
): boolean {
if (
params.watcherSessionKey === params.targetSessionKey ||
!isNotifiableWatcherKey(params.watcherSessionKey)
) {
return false;
}
const now = options.now ?? Date.now();
try {
let registered = false;
runOpenClawStateWriteTransaction(({ db }) => {
// Re-watching must not clobber pending-notice cursor state.
if (readCursor(db, params.watcherSessionKey, params.targetSessionKey)) {
registered = true;
return;
}
const agentId = params.targetAgentId ?? resolveAgentIdFromSessionKey(params.targetSessionKey);
const head = executeSqliteQueryTakeFirstSync(
db,
getSessionStateKysely(db)
.selectFrom("session_state_heads")
.select("last_sequence")
.where("session_key", "=", params.targetSessionKey)
.where("agent_id", "=", agentId),
);
// Seed at the current head: the watcher is synced now; only future changes notify.
upsertSeedCursor({
db,
watcherSessionKey: params.watcherSessionKey,
targetSessionKey: params.targetSessionKey,
sequence: normalizeOptionalSqliteNumber(head?.last_sequence) ?? 0,
now,
});
registered = true;
}, options);
return registered;
} catch (error) {
log.warn(`failed to register session state watch: ${String(error)}`);
return false;
}
}
/** Record a direct human turn when the target has a parent or registered watcher. */
export function recordSessionHumanDirectMessage(params: {
sessionKey: string;
entry?: SessionEntry;
@@ -823,7 +910,12 @@ export function recordSessionHumanDirectMessage(params: {
runId?: string;
}): void {
const watcherSessionKey = params.entry?.spawnedBy ?? params.entry?.parentSessionKey;
if (params.actor.actorType !== "human" || !watcherSessionKey) {
if (params.actor.actorType !== "human") {
return;
}
// Unparented sessions record only when someone explicitly watches them: one
// indexed existence probe keeps ordinary un-watched human turns write-free.
if (!watcherSessionKey && !hasSessionStateWatchers(params.sessionKey)) {
return;
}
recordSessionStateEvent({
@@ -835,7 +927,7 @@ export function recordSessionHumanDirectMessage(params: {
...(params.actor.actorId ? { actorId: params.actor.actorId } : {}),
runId: params.runId,
summary: `human message via ${params.channel?.trim() || "unknown"}`,
watcherSessionKeys: [watcherSessionKey],
...(watcherSessionKey ? { watcherSessionKeys: [watcherSessionKey] } : {}),
});
}

View File

@@ -1298,7 +1298,7 @@
},
{
"deferLoading": true,
"description": "Message visible session by sessionKey/label, or configured agent by agentId; sessionKey wins redundant label. Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available.",
"description": "Message visible session by sessionKey/label, or configured agent by agentId; sessionKey wins redundant label. Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available. watch:true: notice arrives when others later change target session.",
"inputSchema": {
"properties": {
"agentId": {
@@ -1320,6 +1320,9 @@
"timeoutSeconds": {
"minimum": 0,
"type": "integer"
},
"watch": {
"type": "boolean"
}
},
"required": ["message"],

View File

@@ -1330,7 +1330,7 @@
},
{
"deferLoading": true,
"description": "Message visible session by sessionKey/label, or configured agent by agentId; sessionKey wins redundant label. Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available.",
"description": "Message visible session by sessionKey/label, or configured agent by agentId; sessionKey wins redundant label. Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available. watch:true: notice arrives when others later change target session.",
"inputSchema": {
"properties": {
"agentId": {
@@ -1352,6 +1352,9 @@
"timeoutSeconds": {
"minimum": 0,
"type": "integer"
},
"watch": {
"type": "boolean"
}
},
"required": ["message"],

View File

@@ -1294,7 +1294,7 @@
},
{
"deferLoading": true,
"description": "Message visible session by sessionKey/label, or configured agent by agentId; sessionKey wins redundant label. Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available.",
"description": "Message visible session by sessionKey/label, or configured agent by agentId; sessionKey wins redundant label. Thread chats rejected: target parent channel. Missing configured-agent main created. Waits for reply when available. watch:true: notice arrives when others later change target session.",
"inputSchema": {
"properties": {
"agentId": {
@@ -1316,6 +1316,9 @@
"timeoutSeconds": {
"minimum": 0,
"type": "integer"
},
"watch": {
"type": "boolean"
}
},
"required": ["message"],

View File

@@ -208,8 +208,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 52506,
"roughTokens": 13127
"chars": 52644,
"roughTokens": 13161
},
"openClawDeveloperInstructions": {
"chars": 3262,
@@ -220,8 +220,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6947
},
"totalWithDynamicToolsJson": {
"chars": 80295,
"roughTokens": 20074
"chars": 80433,
"roughTokens": 20109
},
"userInputText": {
"chars": 1442,

View File

@@ -208,8 +208,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 52233,
"roughTokens": 13059
"chars": 52371,
"roughTokens": 13093
},
"openClawDeveloperInstructions": {
"chars": 2153,
@@ -220,8 +220,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6568
},
"totalWithDynamicToolsJson": {
"chars": 78504,
"roughTokens": 19626
"chars": 78642,
"roughTokens": 19661
},
"userInputText": {
"chars": 1033,

View File

@@ -209,8 +209,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 0
},
"dynamicToolsJson": {
"chars": 53523,
"roughTokens": 13381
"chars": 53661,
"roughTokens": 13416
},
"openClawDeveloperInstructions": {
"chars": 2172,
@@ -221,8 +221,8 @@ This is the deterministic model-bound layer stack OpenClaw can snapshot for the
"roughTokens": 6702
},
"totalWithDynamicToolsJson": {
"chars": 80333,
"roughTokens": 20084
"chars": 80471,
"roughTokens": 20118
},
"userInputText": {
"chars": 1271,