Files
openclaw/packages/gateway-protocol
Peter Steinberger 73bdb4b924 feat(agents): record run-end worktree cleanup outcome; prove Workboard dirty retention (#120434)
* feat(agents): record run-end worktree cleanup outcome

Persist removed, retained, and failed run-end cleanup outcomes on managed worktree records. Operators and QA can inspect the durable fact through worktrees.list and openclaw worktrees list --json.

Release note: Managed worktree run-end cleanup now records why a checkout was removed or retained in worktree list JSON.

* test(qa): prove dirty worktree retention outcome

* chore(protocol): regenerate swift gateway models

* fix(agents): harden worktree cleanup recovery

Register run_end_cleanup_json as a lazy compatible column so same-version v6 index repair and read-only doctor migration can recover databases created before the column existed.

Type removal contention at the registry boundary; unexpected claim failures now best-effort record a bounded failed outcome and rethrow the original error.

* fix(ci): clear repo-wide lint debt blocking merge gates

The red-main landing rule requires this PR to repair repository-wide merge-gate debt instead of bypassing it. Apply the current lint contracts mechanically and split turn-transition coverage into a concept-named sibling with per-file-safe test state.

Exact line delta: +676/-574 (net +102) across 44 test/support files.

* fix(ci): preserve cached health refresh proof

Require the public refresh call to exist before accepting that sensitive fields were omitted, so the boundary proof cannot pass on a missing call.

* fix(ci): correct test typing left by the lint sweep

Literal-widened totalTokensVersion fixtures, a WebSocket RawData overload
mismatch, and the protocol schema document cast broke check-test-types
after the repo-wide lint repair. Aligns the fixtures with SessionEntry,
narrows Buffer handling per RawData, and keeps the JSON-shaped undefined
omission under structuredClone.

* test(agents): reuse upstream resource-loader test support

The session-loop split and #120463's helper extraction landed the same
createResourceLoader/createCompactionHandlers twice; the rebase kept both,
orphaning main's agent-session-loop-resource-loader.test-support.ts and
failing the dead-code gate. Import the upstream helpers and delete the
duplicates.

* fix(agents): reject finalized rows at the worktree removal claim

Address the accepted ClawSweeper late-claim finding by rereading and rejecting missing or finalized worktree rows inside the synchronous removal-claim transaction.

Preserve the authoritative cleanup invariant: finalized contenders record nothing, while retained-busy is written only while the row remains live.

* refactor(agents): reuse registry update for busy outcomes

Keep the live-row conditional write in the canonical registry update path so the finalized-claim repair stays below the registry max-lines ratchet without weakening the authoritative-outcome invariant.

* test(agents): drop session test duplicates after rebase

Keep current main as the canonical owner of next-turn lifecycle coverage and correctness test support after replaying the older lint-debt split.

* fix(agents): guard post-abort cleanup outcomes against finalization

After abortWorktreeRemoval releases a stale remover's claim, its retained or
failed write raced a finalizing remover and could overwrite the authoritative
removed-lossless fact. Route every retained/failed write through the live-row
condition; only the finalizing remover's own removed-lossless write stays
unconditional.

* fix(agents): persist the removal outcome atomically with finalization

A delayed removed-lossless write after remove() finalized could race a
restore plus newer cleanup and overwrite the newer operator-visible fact.
The run-end outcome now rides remove()'s finalization update; every other
cleanup write stays live-row conditional, so no post-finalize write path
remains.

* test(qa): restore strict cached-health contract assertions

The lint sweep's Boolean() coercions let truthy non-booleans satisfy the
wire-typed cached-meta contract. Assert the literal boolean for unknown-typed
fields and use nullish-coalesced strict equivalents for boolean chains.

* fix(agents): clear the stale cleanup outcome when restoring a worktree

A restored checkout begins a new lifecycle; leaving the removed-lossless
fact on the live row showed operators a stale result until the next
cleanup. Restore clears the recorded outcome and the regression asserts
the cleared state before the next cleanup records fresh truth.

* fix(agents): scope stale cleanup outcomes to their observed lifecycle

A stale remover's retained/failed write raced a concurrent remove-plus-
restore: the revived row is live again, so the live-row condition alone
could stamp a prior-lifecycle outcome. Condition those writes on the
activity stamp the remover observed; restore bumps lastActiveAt, making
any prior-lifecycle write a no-op.

* fix(agents): advance the restore activity stamp within one millisecond

Stale cleanup writes fence on the activity stamp they observed; a restore
completing in the same millisecond could revive the row with an identical
stamp and let the fence match. Restore now always advances past the
stored value, and the ABA regression pins the clock to prove the
same-millisecond case.
2026-08-08 20:32:11 -07:00
..

@openclaw/gateway-protocol

Typed schemas, inferred TypeScript types, and runtime validators for the OpenClaw Gateway WebSocket protocol.

The current wire protocol is version 4. General clients must use v4; authenticated node clients and lightweight probes may use the N-1 window during rolling upgrades. See the Gateway protocol specification for transport, authentication, roles, scopes, and complete frame examples.

Versioning

Package versions follow the OpenClaw calendar release train: YYYY.M.PATCH, with the same prerelease suffix when applicable. A package version therefore identifies the OpenClaw source release that produced the schemas; it is not the wire protocol number.

The wire protocol integer is versioned separately. Its current value is exported as PROTOCOL_VERSION from @openclaw/gateway-protocol/version. Gateway protocol changes are additive first. An incompatible wire change requires an explicit protocol-version decision and coordinated client follow-through. See CHANGELOG.md for the wire and schema history.

Install

npm install @openclaw/gateway-protocol

Entry points

  • @openclaw/gateway-protocol exports runtime validators, selected schemas, error formatting, and their TypeScript types. This is the main TypeBox-backed entry.
  • @openclaw/gateway-protocol/schema exports the TypeBox schema graph, including the ProtocolSchemas registry used by generators.
  • @openclaw/gateway-protocol/frame-guards exports dependency-free structural guards for gateway event and response envelopes.
  • @openclaw/gateway-protocol/client-info exports client ID, mode, and capability registries plus normalization helpers.
  • @openclaw/gateway-protocol/connect-error-details exports structured connect error readers and recovery metadata.
  • @openclaw/gateway-protocol/gateway-error-details exports helpers for reading structured details from general gateway errors.
  • @openclaw/gateway-protocol/startup-unavailable exports startup retry constants and helpers.
  • @openclaw/gateway-protocol/version exports the current and minimum accepted protocol versions.

The frame-guards, client-info, connect-error-details, gateway-error-details, startup-unavailable, and version entry points are TypeBox-free. Prefer them when a browser bundle only needs envelope dispatch, handshake constants, or reconnect policy. This also avoids runtime compilation in CSP-sensitive consumers. The root and schema entry points provide the full validation surface and depend on TypeBox.

Validate an inbound frame

The compiled validators are callable type guards. Their errors property contains the most recent validation errors.

import { formatValidationErrors, validateRequestFrame } from "@openclaw/gateway-protocol";

const frame: unknown = JSON.parse(inboundText);

if (!validateRequestFrame(frame)) {
  throw new Error(formatValidationErrors(validateRequestFrame.errors));
}

console.log(frame.id, frame.method);

validateRequestFrame validates the request envelope. Dispatch code must also use the validator for the selected method's params; the root entry point exports those validators as validate*Params functions.

Guard an event without TypeBox

Use the lightweight guards when code only needs safe frame discrimination. They check dispatch-critical envelope fields and intentionally allow additive payload fields.

import { isGatewayEventFrame } from "@openclaw/gateway-protocol/frame-guards";

const frame: unknown = JSON.parse(inboundText);

if (isGatewayEventFrame(frame)) {
  console.log(frame.event, frame.seq);
}

Build handshake version and capability fields

Protocol levels and client capabilities live in TypeBox-free entry points.

import { GATEWAY_CLIENT_CAPS } from "@openclaw/gateway-protocol/client-info";
import { MIN_CLIENT_PROTOCOL_VERSION, PROTOCOL_VERSION } from "@openclaw/gateway-protocol/version";

const handshake = {
  minProtocol: MIN_CLIENT_PROTOCOL_VERSION,
  maxProtocol: PROTOCOL_VERSION,
  caps: [GATEWAY_CLIENT_CAPS.TOOL_EVENTS],
};

Nodes and probes use MIN_NODE_PROTOCOL_VERSION and MIN_PROBE_PROTOCOL_VERSION, respectively. A capability advertises client support; it does not grant authorization.

Contract notes

Session identifiers

Several identifier names coexist because they identify different things:

  • key is the established logical session selector used by most sessions.* CRUD, send, subscription, patch, reset, delete, compaction, and usage methods. A key can be canonicalized or resolved within an agent's session store.
  • sessionKey names the same logical routing identity where the contract needs to make that meaning explicit. chat.*, session file and diff APIs, transcript branch/rewind/fork APIs, agent events, and channel delivery payloads use this spelling.
  • sessionId is the opaque stored transcript or runtime instance ID. Session results may return it beside a key. Talk, terminal, worker, and selected channel protocols also use sessionId for their own concrete session instances; do not substitute a logical session key there.

Follow each method schema rather than converting fields based on their spelling. sessions.resolve is the explicit bridge when a caller has a key, raw session ID, label, Control UI short ID, or parent/agent scope.

Intentionally open fields

The schema graph is strict by default, but roughly 60 fields intentionally use Type.Unknown() passthroughs. The main clusters are transport-owned channel payloads, logs-chat message and attachment passthrough, worker and node tool arguments/results, and the dynamic config.schema response. Frame params, payload, and error details are also open at the envelope layer because the selected method, event, or error code owns their concrete shape.

Do not treat these fields as validated domain objects. Narrow them at their owner boundary before reading nested values.

Machine-readable schema

protocol.schema.json ships in the npm tarball as the generated machine-readable contract. It contains the frame union, named schema definitions, and core method metadata. It is generated during prepack and is not committed to the repository.

Method discovery

The hello-ok.features.methods list is conservative discovery, not a complete enumeration of every callable method. It reflects the methods the connected Gateway intentionally advertises. Core-internal, role-specific, plugin-provided, or otherwise non-advertised methods can have valid schemas without appearing in that list. Clients should use discovery to enable optional UI, not to reject an otherwise documented method contract.