mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 21:10:42 +00:00
Merge branch 'main' into fix/control-ui-sender-metadata-stream
This commit is contained in:
@@ -32,6 +32,14 @@ pnpm crabbox:run -- --help | sed -n '1,120p'
|
||||
Even if config still says AWS, maintainer validation should normally pass
|
||||
`--provider blacksmith-testbox`.
|
||||
- Prefer local targeted tests for tight edit loops. Broad gates belong remote.
|
||||
- Do not treat inherited shell env as operator intent. In particular,
|
||||
`OPENCLAW_LOCAL_CHECK_MODE=throttled` from the local shell is not permission
|
||||
to move broad `pnpm check:changed`, `pnpm test:changed`, full `pnpm test`, or
|
||||
lint/typecheck fan-out onto the laptop.
|
||||
- Only use `OPENCLAW_LOCAL_CHECK_MODE=throttled|full` when the user explicitly
|
||||
asks for local proof in the current task. If Testbox is queued or capacity is
|
||||
constrained, report the blocker and keep only targeted local edit-loop checks
|
||||
running.
|
||||
|
||||
## macOS And Windows Targets
|
||||
|
||||
@@ -198,6 +206,10 @@ Common Crabbox-only failures:
|
||||
printed Actions URL.
|
||||
- Cleanup uncertainty: run `blacksmith testbox list` and stop only boxes you
|
||||
created.
|
||||
- Testbox queued/capacity pressure: do not convert a broad changed gate or full
|
||||
suite into local `OPENCLAW_LOCAL_CHECK_MODE=throttled pnpm ...`. Leave the
|
||||
remote lane queued, switch to a narrower targeted local check, or stop and
|
||||
report the capacity blocker.
|
||||
|
||||
If Crabbox cannot dispatch, sync, attach, or stop but Blacksmith itself works,
|
||||
use direct Blacksmith from the repo root:
|
||||
@@ -229,21 +241,6 @@ Raw Blacksmith footguns:
|
||||
- Treat `blacksmith testbox list` as cleanup diagnostics, not a shared reusable
|
||||
queue.
|
||||
|
||||
Blacksmith queue/outage mode:
|
||||
|
||||
```sh
|
||||
blacksmith --version
|
||||
blacksmith testbox list --all
|
||||
blacksmith testbox status --id <tbx_id>
|
||||
```
|
||||
|
||||
If the CLI can list/status boxes but new warmups stay `queued` with no IP or
|
||||
Actions run URL after a couple of minutes, treat it as Blacksmith provider,
|
||||
org-limit, billing, or queue pressure. Stop the queued ids you created and do
|
||||
not warm more boxes into the same stalled queue. Check the Blacksmith dashboard,
|
||||
billing, and org limits out-of-band, then use Owned Cloud Fallback below for
|
||||
maintainer proof.
|
||||
|
||||
Escalate to owned AWS/Hetzner only when Blacksmith is down, quota-limited,
|
||||
missing the needed environment, or owned capacity is the explicit goal. Use the
|
||||
Owned Cloud Fallback section below.
|
||||
@@ -277,9 +274,6 @@ Important Blacksmith footguns:
|
||||
|
||||
- Always run from repo root. The CLI syncs the current directory.
|
||||
- Raw commit SHAs are not reliable `warmup --ref` refs; use a branch or tag.
|
||||
- If `blacksmith testbox list --all` works but warmups stay `queued`, this is
|
||||
not a Crabbox bug. Stop the queued ids and switch to owned AWS/Hetzner instead
|
||||
of retrying.
|
||||
- If auth is missing and browser auth is acceptable:
|
||||
|
||||
```sh
|
||||
@@ -291,45 +285,8 @@ blacksmith auth login --non-interactive --organization openclaw
|
||||
Use AWS/Hetzner only when Blacksmith is down, quota-limited, missing the needed
|
||||
environment, or owned capacity is explicitly the goal.
|
||||
|
||||
When AWS capacity is under pressure, do not start with `class=beast`.
|
||||
`beast` begins at 48xlarge instances and can burn 192 vCPU quota per request.
|
||||
OpenClaw's owned-cloud default is `standard`; escalate to `fast`, then `large`,
|
||||
and only use `beast` when the work is explicitly CPU-bound and the smaller class
|
||||
already failed the goal.
|
||||
Keep capacity hints enabled so brokered AWS leases print selected region/market,
|
||||
quota pressure, Spot fallback, and high-pressure class warnings. The OpenClaw
|
||||
repo config sets `capacity.hints: true`; use `CRABBOX_CAPACITY_HINTS=0` only
|
||||
when debugging hint rendering itself.
|
||||
|
||||
Use `beast` only for exceptional lanes:
|
||||
|
||||
- full-suite or all-plugin Docker matrices where wall time is dominated by CPU,
|
||||
not dependency install or network;
|
||||
- release/blocker validation where a maintainer explicitly asks for the largest
|
||||
owned AWS class;
|
||||
- performance profiling where the point is to compare high-core behavior.
|
||||
|
||||
Do not use `beast` for `pnpm check:changed`, focused tests, docs-only work,
|
||||
ordinary lint/typecheck, small E2E repros, or Blacksmith outage triage. Those
|
||||
should use `standard` first and `fast` only when the extra cores materially help.
|
||||
|
||||
Preferred AWS pressure-relief flow:
|
||||
|
||||
```sh
|
||||
CRABBOX_CAPACITY_REGIONS=eu-west-1,eu-west-2,eu-central-1,us-east-1,us-west-2 \
|
||||
pnpm crabbox:warmup -- --provider aws --class standard --market on-demand --idle-timeout 90m
|
||||
pnpm crabbox:hydrate -- --id <cbx_id-or-slug>
|
||||
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm check:changed"
|
||||
pnpm crabbox:stop -- <cbx_id-or-slug>
|
||||
```
|
||||
|
||||
Use `--market spot` only when testing Spot behavior or saving cost matters more
|
||||
than launch reliability. Use `--market on-demand` when diagnosing quota/capacity
|
||||
because it removes Spot market churn from the failure.
|
||||
|
||||
```sh
|
||||
CRABBOX_CAPACITY_REGIONS=eu-west-1,eu-west-2,eu-central-1,us-east-1,us-west-2 \
|
||||
pnpm crabbox:warmup -- --provider aws --class fast --market on-demand --idle-timeout 90m
|
||||
pnpm crabbox:warmup -- --provider aws --class beast --market on-demand --idle-timeout 90m
|
||||
pnpm crabbox:hydrate -- --id <cbx_id-or-slug>
|
||||
pnpm crabbox:run -- --id <cbx_id-or-slug> --timing-json --shell -- "env NODE_OPTIONS=--max-old-space-size=4096 OPENCLAW_TEST_PROJECTS_PARALLEL=6 OPENCLAW_VITEST_MAX_WORKERS=1 OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS=900000 pnpm test:changed"
|
||||
pnpm crabbox:stop -- <cbx_id-or-slug>
|
||||
@@ -339,9 +296,27 @@ Install/auth for owned Crabbox if needed:
|
||||
|
||||
```sh
|
||||
brew install openclaw/tap/crabbox
|
||||
printf '%s' "$CRABBOX_COORDINATOR_TOKEN" | crabbox login --url https://crabbox.openclaw.ai --provider aws --token-stdin
|
||||
crabbox login --url https://crabbox.openclaw.ai --provider aws
|
||||
```
|
||||
|
||||
New users should self-resolve broker auth before anyone asks for AWS keys:
|
||||
|
||||
```sh
|
||||
crabbox config show
|
||||
crabbox doctor
|
||||
crabbox whoami
|
||||
```
|
||||
|
||||
- If broker auth is missing, run `crabbox login --url https://crabbox.openclaw.ai --provider aws`.
|
||||
- If the CLI asks for `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, or AWS
|
||||
profile setup during normal OpenClaw validation, assume the agent selected
|
||||
the wrong path. Use brokered `crabbox login`, `--provider blacksmith-testbox`,
|
||||
or an existing brokered lease before asking the user for cloud credentials.
|
||||
- Ask for AWS keys only for explicit direct-provider/account administration,
|
||||
not for normal brokered OpenClaw proof.
|
||||
- Trusted automation may still use
|
||||
`printf '%s' "$CRABBOX_COORDINATOR_TOKEN" | crabbox login --url https://crabbox.openclaw.ai --provider aws --token-stdin`.
|
||||
|
||||
macOS config lives at:
|
||||
|
||||
```text
|
||||
|
||||
@@ -14,7 +14,6 @@ query-filters:
|
||||
- security
|
||||
|
||||
paths:
|
||||
- extensions/bluebubbles/src
|
||||
- extensions/discord/src
|
||||
- extensions/feishu/src
|
||||
- extensions/googlechat/src
|
||||
|
||||
28
.github/codeql/codeql-network-runtime-boundary-critical-quality.yml
vendored
Normal file
28
.github/codeql/codeql-network-runtime-boundary-critical-quality.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: openclaw-codeql-network-runtime-boundary-critical-quality
|
||||
|
||||
disable-default-queries: true
|
||||
|
||||
queries:
|
||||
- uses: ./.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql
|
||||
- uses: ./.github/codeql/openclaw-boundary/queries/managed-proxy-runtime-mutation.ql
|
||||
|
||||
paths:
|
||||
- src
|
||||
- extensions
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
- "**/coverage"
|
||||
- "**/*.generated.ts"
|
||||
- "**/*.bundle.js"
|
||||
- "**/*-runtime.js"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/*.e2e.test.ts"
|
||||
- "**/*.e2e.test.tsx"
|
||||
- "**/*test-support*"
|
||||
- "**/*test-helper*"
|
||||
- "**/*mock*"
|
||||
- "**/*fixture*"
|
||||
- "**/*bench*"
|
||||
- "extensions/diffs/assets/**"
|
||||
30
.github/codeql/openclaw-boundary/codeql-pack.lock.yml
vendored
Normal file
30
.github/codeql/openclaw-boundary/codeql-pack.lock.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
lockVersion: 1.0.0
|
||||
dependencies:
|
||||
codeql/concepts:
|
||||
version: 0.0.22
|
||||
codeql/controlflow:
|
||||
version: 2.0.32
|
||||
codeql/dataflow:
|
||||
version: 2.1.4
|
||||
codeql/javascript-all:
|
||||
version: 2.6.28
|
||||
codeql/mad:
|
||||
version: 1.0.48
|
||||
codeql/regex:
|
||||
version: 1.0.48
|
||||
codeql/ssa:
|
||||
version: 2.0.24
|
||||
codeql/threat-models:
|
||||
version: 1.0.48
|
||||
codeql/tutorial:
|
||||
version: 1.0.48
|
||||
codeql/typetracking:
|
||||
version: 2.0.32
|
||||
codeql/util:
|
||||
version: 2.0.35
|
||||
codeql/xml:
|
||||
version: 1.0.48
|
||||
codeql/yaml:
|
||||
version: 1.0.48
|
||||
compiled: false
|
||||
6
.github/codeql/openclaw-boundary/qlpack.yml
vendored
Normal file
6
.github/codeql/openclaw-boundary/qlpack.yml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
name: openclaw/codeql-boundary-queries
|
||||
version: 0.0.0
|
||||
library: false
|
||||
dependencies:
|
||||
codeql/javascript-all: 2.6.28
|
||||
extractor: javascript
|
||||
325
.github/codeql/openclaw-boundary/queries/managed-proxy-runtime-mutation.ql
vendored
Normal file
325
.github/codeql/openclaw-boundary/queries/managed-proxy-runtime-mutation.ql
vendored
Normal file
@@ -0,0 +1,325 @@
|
||||
/**
|
||||
* @name Managed proxy runtime mutation
|
||||
* @description Proxy-related process.env and GLOBAL_AGENT runtime mutations must stay in managed proxy owner scopes.
|
||||
* @kind problem
|
||||
* @problem.severity error
|
||||
* @precision high
|
||||
* @id js/openclaw/managed-proxy-runtime-mutation
|
||||
* @tags maintainability
|
||||
* security
|
||||
* external/cwe/cwe-441
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
predicate forbiddenEnvKey(string key) {
|
||||
key =
|
||||
[
|
||||
"HTTP_PROXY",
|
||||
"HTTPS_PROXY",
|
||||
"http_proxy",
|
||||
"https_proxy",
|
||||
"NO_PROXY",
|
||||
"no_proxy",
|
||||
"GLOBAL_AGENT_HTTP_PROXY",
|
||||
"GLOBAL_AGENT_HTTPS_PROXY",
|
||||
"GLOBAL_AGENT_NO_PROXY",
|
||||
"GLOBAL_AGENT_FORCE_GLOBAL_AGENT",
|
||||
"OPENCLAW_PROXY_ACTIVE",
|
||||
"OPENCLAW_PROXY_LOOPBACK_MODE"
|
||||
]
|
||||
}
|
||||
|
||||
predicate forbiddenGlobalAgentKey(string key) { key = ["HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY"] }
|
||||
|
||||
predicate relevantSourceFile(File file) {
|
||||
exists(string path |
|
||||
path = file.getRelativePath() and
|
||||
path.regexpMatch("^(src|extensions)/.*\\.(ts|mts|js|mjs)$") and
|
||||
not path.regexpMatch(".*\\.(test|spec)\\.(ts|mts|js|mjs)$") and
|
||||
not path.regexpMatch(".*\\.(test-utils|test-harness|e2e-harness)\\.ts$") and
|
||||
not path.regexpMatch(".*/test-support/.*") and
|
||||
not path.regexpMatch(".*/vendor/.*") and
|
||||
not path.regexpMatch(".*\\.min\\.js$") and
|
||||
not path.regexpMatch("^extensions/diffs/assets/.*")
|
||||
)
|
||||
}
|
||||
|
||||
predicate namedExpr(Expr expr, string name) {
|
||||
expr.getUnderlyingValue().(Identifier).getName() = name
|
||||
}
|
||||
|
||||
predicate directProcessEnvExpr(Expr expr) {
|
||||
exists(PropAccess access |
|
||||
expr.getUnderlyingValue() = access and
|
||||
access.getPropertyName() = "env" and
|
||||
namedExpr(access.getBase(), "process")
|
||||
)
|
||||
}
|
||||
|
||||
predicate envAlias(Variable variable) {
|
||||
exists(VariableDeclarator decl |
|
||||
decl.getBindingPattern().getAVariable() = variable and
|
||||
directProcessEnvExpr(decl.getInit())
|
||||
)
|
||||
or
|
||||
exists(VariableDeclarator decl, ObjectPattern pattern, PropertyPattern property |
|
||||
decl.getBindingPattern() = pattern and
|
||||
namedExpr(decl.getInit(), "process") and
|
||||
property = pattern.getAPropertyPattern() and
|
||||
property.getName() = "env" and
|
||||
property.getValuePattern().(BindingPattern).getAVariable() = variable
|
||||
)
|
||||
}
|
||||
|
||||
predicate processEnvExpr(Expr expr) {
|
||||
directProcessEnvExpr(expr)
|
||||
or
|
||||
exists(VarAccess access |
|
||||
expr.getUnderlyingValue() = access and
|
||||
envAlias(access.getVariable())
|
||||
)
|
||||
}
|
||||
|
||||
predicate stringConst(Variable variable, string value) {
|
||||
exists(VariableDeclarator decl |
|
||||
decl.getBindingPattern().getAVariable() = variable and
|
||||
value = decl.getInit().getStringValue()
|
||||
)
|
||||
}
|
||||
|
||||
predicate stringArrayContains(Variable variable, string value) {
|
||||
exists(VariableDeclarator decl, ArrayExpr array, Expr element |
|
||||
decl.getBindingPattern().getAVariable() = variable and
|
||||
decl.getInit().getUnderlyingValue() = array and
|
||||
element = array.getAnElement().getUnderlyingValue() and
|
||||
value = element.getStringValue()
|
||||
)
|
||||
or
|
||||
exists(VariableDeclarator decl, ArrayExpr array, SpreadElement spread, VarAccess access |
|
||||
decl.getBindingPattern().getAVariable() = variable and
|
||||
decl.getInit().getUnderlyingValue() = array and
|
||||
spread = array.getAnElement().getUnderlyingValue() and
|
||||
spread.getOperand().getUnderlyingValue() = access and
|
||||
stringArrayContains(access.getVariable(), value)
|
||||
)
|
||||
}
|
||||
|
||||
predicate forbiddenEnvLoopVariable(Variable variable) {
|
||||
exists(ForOfStmt loop, VarAccess domain, string key |
|
||||
variable = loop.getAnIterationVariable() and
|
||||
loop.getIterationDomain().getUnderlyingValue() = domain and
|
||||
stringArrayContains(domain.getVariable(), key) and
|
||||
forbiddenEnvKey(key)
|
||||
)
|
||||
}
|
||||
|
||||
predicate envKeyExprForbidden(Expr keyExpr) {
|
||||
forbiddenEnvKey(keyExpr.getStringValue())
|
||||
or
|
||||
exists(VarAccess access, string key |
|
||||
keyExpr.getUnderlyingValue() = access and
|
||||
stringConst(access.getVariable(), key) and
|
||||
forbiddenEnvKey(key)
|
||||
)
|
||||
or
|
||||
exists(VarAccess access |
|
||||
keyExpr.getUnderlyingValue() = access and
|
||||
forbiddenEnvLoopVariable(access.getVariable())
|
||||
)
|
||||
}
|
||||
|
||||
predicate globalAgentKeyExprForbidden(Expr keyExpr) {
|
||||
forbiddenGlobalAgentKey(keyExpr.getStringValue())
|
||||
or
|
||||
exists(VarAccess access, string key |
|
||||
keyExpr.getUnderlyingValue() = access and
|
||||
stringConst(access.getVariable(), key) and
|
||||
forbiddenGlobalAgentKey(key)
|
||||
)
|
||||
}
|
||||
|
||||
predicate directGlobalExpr(Expr expr) {
|
||||
namedExpr(expr, "global")
|
||||
or
|
||||
namedExpr(expr, "globalThis")
|
||||
}
|
||||
|
||||
predicate globalAlias(Variable variable) {
|
||||
exists(VariableDeclarator decl |
|
||||
decl.getBindingPattern().getAVariable() = variable and
|
||||
directGlobalExpr(decl.getInit())
|
||||
)
|
||||
}
|
||||
|
||||
predicate globalExpr(Expr expr) {
|
||||
directGlobalExpr(expr)
|
||||
or
|
||||
exists(VarAccess access |
|
||||
expr.getUnderlyingValue() = access and
|
||||
globalAlias(access.getVariable())
|
||||
)
|
||||
}
|
||||
|
||||
predicate directGlobalAgentExpr(Expr expr) {
|
||||
exists(PropAccess access |
|
||||
expr.getUnderlyingValue() = access and
|
||||
access.getPropertyName() = "GLOBAL_AGENT" and
|
||||
globalExpr(access.getBase())
|
||||
)
|
||||
}
|
||||
|
||||
predicate globalAgentAlias(Variable variable) {
|
||||
exists(VariableDeclarator decl |
|
||||
decl.getBindingPattern().getAVariable() = variable and
|
||||
directGlobalAgentExpr(decl.getInit())
|
||||
)
|
||||
}
|
||||
|
||||
predicate globalAgentExpr(Expr expr) {
|
||||
directGlobalAgentExpr(expr)
|
||||
or
|
||||
exists(VarAccess access |
|
||||
expr.getUnderlyingValue() = access and
|
||||
globalAgentAlias(access.getVariable())
|
||||
)
|
||||
}
|
||||
|
||||
predicate envMutationTarget(Expr target) {
|
||||
exists(PropAccess access |
|
||||
target.getUnderlyingReference() = access and
|
||||
processEnvExpr(access.getBase()) and
|
||||
(
|
||||
forbiddenEnvKey(access.getPropertyName())
|
||||
or
|
||||
envKeyExprForbidden(access.getPropertyNameExpr())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
predicate globalAgentMutationTarget(Expr target) {
|
||||
globalAgentExpr(target)
|
||||
or
|
||||
exists(PropAccess access |
|
||||
target.getUnderlyingReference() = access and
|
||||
globalAgentExpr(access.getBase()) and
|
||||
(
|
||||
forbiddenGlobalAgentKey(access.getPropertyName())
|
||||
or
|
||||
globalAgentKeyExprForbidden(access.getPropertyNameExpr())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
predicate objectPropertyWithKey(Expr expr, string key) {
|
||||
exists(ObjectExpr object, Property property |
|
||||
expr.getUnderlyingValue() = object and
|
||||
property = object.getAProperty() and
|
||||
property.getName() = key
|
||||
)
|
||||
}
|
||||
|
||||
Expr managedProxyRuntimeMutation() {
|
||||
exists(Assignment assignment |
|
||||
result = assignment and
|
||||
(
|
||||
envMutationTarget(assignment.getTarget())
|
||||
or
|
||||
globalAgentMutationTarget(assignment.getTarget())
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(DeleteExpr delete |
|
||||
result = delete and
|
||||
(
|
||||
envMutationTarget(delete.getOperand())
|
||||
or
|
||||
globalAgentMutationTarget(delete.getOperand())
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(MethodCallExpr call |
|
||||
result = call and
|
||||
namedExpr(call.getReceiver(), "Object") and
|
||||
call.getMethodName() = "assign" and
|
||||
(
|
||||
processEnvExpr(call.getArgument(0)) and
|
||||
exists(string key |
|
||||
forbiddenEnvKey(key) and
|
||||
objectPropertyWithKey(call.getArgument(1), key)
|
||||
)
|
||||
or
|
||||
globalAgentExpr(call.getArgument(0)) and
|
||||
exists(string key |
|
||||
forbiddenGlobalAgentKey(key) and
|
||||
objectPropertyWithKey(call.getArgument(1), key)
|
||||
)
|
||||
)
|
||||
)
|
||||
or
|
||||
exists(MethodCallExpr call |
|
||||
result = call and
|
||||
namedExpr(call.getReceiver(), "Object") and
|
||||
call.getMethodName() = "defineProperty" and
|
||||
(
|
||||
processEnvExpr(call.getArgument(0)) and
|
||||
envKeyExprForbidden(call.getArgument(1))
|
||||
or
|
||||
globalAgentExpr(call.getArgument(0)) and
|
||||
globalAgentKeyExprForbidden(call.getArgument(1))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
predicate allowedFunctionOwnerScope(Expr mutation, string path, string functionName) {
|
||||
exists(Function owner |
|
||||
mutation.getFile().getRelativePath() = path and
|
||||
owner.getFile() = mutation.getFile() and
|
||||
owner.getName() = functionName and
|
||||
mutation.getParent*() = owner.getBody()
|
||||
)
|
||||
}
|
||||
|
||||
predicate allowedMethodOwnerScope(Expr mutation, string path, string methodName) {
|
||||
exists(MethodDeclaration method |
|
||||
mutation.getFile().getRelativePath() = path and
|
||||
method.getFile() = mutation.getFile() and
|
||||
method.getDeclaringType().getName() + "." + method.getName() = methodName and
|
||||
mutation.getParent*() = method.getBody().getBody()
|
||||
)
|
||||
}
|
||||
|
||||
predicate allowedManagedProxyRuntimeMutation(Expr mutation) {
|
||||
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts", "applyProxyEnv")
|
||||
or
|
||||
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts", "restoreProxyEnv")
|
||||
or
|
||||
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
|
||||
"restoreGlobalAgentRuntime")
|
||||
or
|
||||
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
|
||||
"restoreNodeHttpStack")
|
||||
or
|
||||
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
|
||||
"bootstrapNodeHttpStack")
|
||||
or
|
||||
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
|
||||
"writeGlobalAgentNoProxy")
|
||||
or
|
||||
allowedFunctionOwnerScope(mutation, "src/infra/net/proxy/proxy-lifecycle.ts",
|
||||
"disableGlobalAgentProxyForIpv6GatewayLoopback")
|
||||
or
|
||||
allowedMethodOwnerScope(mutation, "extensions/browser/src/browser/cdp-proxy-bypass.ts",
|
||||
"NoProxyLeaseManager.acquire")
|
||||
or
|
||||
allowedMethodOwnerScope(mutation, "extensions/browser/src/browser/cdp-proxy-bypass.ts",
|
||||
"NoProxyLeaseManager.release")
|
||||
}
|
||||
|
||||
from Expr mutation
|
||||
where
|
||||
managedProxyRuntimeMutation() = mutation and
|
||||
relevantSourceFile(mutation.getFile()) and
|
||||
not allowedManagedProxyRuntimeMutation(mutation)
|
||||
select mutation,
|
||||
"Only managed proxy owner scopes may mutate proxy-related process.env or GLOBAL_AGENT runtime state."
|
||||
92
.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql
vendored
Normal file
92
.github/codeql/openclaw-boundary/queries/raw-socket-callsite-classification.ql
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @name Raw socket client callsite classification
|
||||
* @description Raw net/tls/http2 client egress must be classified before landing.
|
||||
* @kind problem
|
||||
* @problem.severity error
|
||||
* @precision high
|
||||
* @id js/openclaw/raw-socket-callsite-classification
|
||||
* @tags maintainability
|
||||
* security
|
||||
* external/cwe/cwe-441
|
||||
*/
|
||||
|
||||
import javascript
|
||||
|
||||
predicate rawModule(string moduleName) {
|
||||
moduleName = ["net", "node:net", "tls", "node:tls", "http2", "node:http2"]
|
||||
}
|
||||
|
||||
predicate netModule(string moduleName) { moduleName = ["net", "node:net"] }
|
||||
|
||||
predicate rawConnectMember(string memberName) { memberName = ["connect", "createConnection"] }
|
||||
|
||||
predicate relevantSourceFile(File file) {
|
||||
exists(string path |
|
||||
path = file.getRelativePath() and
|
||||
path.regexpMatch("^(src|extensions)/.*\\.ts$") and
|
||||
not path.regexpMatch(".*\\.(test|spec|test-utils|test-harness|e2e-harness)\\.ts$") and
|
||||
not path.regexpMatch(".*/test-support/.*") and
|
||||
not path.regexpMatch("^extensions/diffs/assets/.*")
|
||||
)
|
||||
}
|
||||
|
||||
Expr rawSocketClientCall() {
|
||||
exists(API::CallNode call, string moduleName, string memberName |
|
||||
rawModule(moduleName) and
|
||||
rawConnectMember(memberName) and
|
||||
call = API::moduleImport(moduleName).getMember(memberName).getACall() and
|
||||
result = call.asExpr()
|
||||
)
|
||||
or
|
||||
exists(string moduleName |
|
||||
netModule(moduleName) and
|
||||
result =
|
||||
DataFlow::moduleMember(moduleName, "Socket")
|
||||
.getAnInstantiation()
|
||||
.getAMethodCall("connect")
|
||||
.asExpr()
|
||||
)
|
||||
}
|
||||
|
||||
predicate allowedOwnerScope(Expr call, string path, string functionName) {
|
||||
exists(Function owner |
|
||||
call.getFile().getRelativePath() = path and
|
||||
owner.getFile() = call.getFile() and
|
||||
owner.getName() = functionName and
|
||||
call.getParent*() = owner.getBody()
|
||||
)
|
||||
}
|
||||
|
||||
predicate allowedRawSocketClientCall(Expr call) {
|
||||
allowedOwnerScope(call, "src/cli/gateway-cli/run-loop.ts", "waitForGatewayPortReady")
|
||||
or
|
||||
allowedOwnerScope(call, "src/infra/ssh-tunnel.ts", "canConnectLocal")
|
||||
or
|
||||
allowedOwnerScope(call, "src/infra/gateway-lock.ts", "checkPortFree")
|
||||
or
|
||||
allowedOwnerScope(call, "src/infra/jsonl-socket.ts", "requestJsonlSocket")
|
||||
or
|
||||
allowedOwnerScope(call, "src/infra/net/http-connect-tunnel.ts", "connectToProxy")
|
||||
or
|
||||
allowedOwnerScope(call, "src/infra/net/http-connect-tunnel.ts", "startTargetTls")
|
||||
or
|
||||
allowedOwnerScope(call, "src/infra/push-apns-http2.ts", "openProxiedApnsHttp2Session")
|
||||
or
|
||||
allowedOwnerScope(call, "src/infra/push-apns-http2.ts", "connectApnsHttp2Session")
|
||||
or
|
||||
allowedOwnerScope(call, "src/proxy-capture/proxy-server.ts", "startDebugProxyServer")
|
||||
or
|
||||
allowedOwnerScope(call, "extensions/irc/src/client.ts", "connectIrcClient")
|
||||
or
|
||||
allowedOwnerScope(call, "extensions/qa-lab/src/lab-server-capture.ts", "probeTcpReachability")
|
||||
or
|
||||
allowedOwnerScope(call, "extensions/qa-lab/src/lab-server-ui.ts", "proxyUpgradeRequest")
|
||||
}
|
||||
|
||||
from Expr call
|
||||
where
|
||||
rawSocketClientCall() = call and
|
||||
relevantSourceFile(call.getFile()) and
|
||||
not allowedRawSocketClientCall(call)
|
||||
select call,
|
||||
"Classify raw net/tls/http2 client egress as managed/proxied, local-only, diagnostic guarded, or documented unsupported before adding this callsite."
|
||||
5
.github/labeler.yml
vendored
5
.github/labeler.yml
vendored
@@ -1,8 +1,3 @@
|
||||
"channel: bluebubbles":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/bluebubbles/**"
|
||||
- "docs/channels/bluebubbles.md"
|
||||
"plugin: azure-speech":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
183
.github/workflows/ci.yml
vendored
183
.github/workflows/ci.yml
vendored
@@ -36,7 +36,6 @@ jobs:
|
||||
# work fan out from a single source of truth.
|
||||
preflight:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -66,11 +65,9 @@ jobs:
|
||||
checks_node_core_dist_matrix: ${{ steps.manifest.outputs.checks_node_core_dist_matrix }}
|
||||
run_check: ${{ steps.manifest.outputs.run_check }}
|
||||
run_check_additional: ${{ steps.manifest.outputs.run_check_additional }}
|
||||
additional_matrix: ${{ steps.manifest.outputs.additional_matrix }}
|
||||
run_build_smoke: ${{ steps.manifest.outputs.run_build_smoke }}
|
||||
run_check_docs: ${{ steps.manifest.outputs.run_check_docs }}
|
||||
run_control_ui_i18n: ${{ steps.manifest.outputs.run_control_ui_i18n }}
|
||||
run_prompt_snapshots: ${{ steps.manifest.outputs.run_prompt_snapshots }}
|
||||
run_checks_windows: ${{ steps.manifest.outputs.run_checks_windows }}
|
||||
checks_windows_matrix: ${{ steps.manifest.outputs.checks_windows_matrix }}
|
||||
run_macos_node: ${{ steps.manifest.outputs.run_macos_node }}
|
||||
@@ -78,12 +75,6 @@ jobs:
|
||||
run_macos_swift: ${{ steps.manifest.outputs.run_macos_swift }}
|
||||
run_android_job: ${{ steps.manifest.outputs.run_android_job }}
|
||||
android_matrix: ${{ steps.manifest.outputs.android_matrix }}
|
||||
runner_4vcpu_ubuntu: ${{ steps.runner_labels.outputs.runner_4vcpu_ubuntu }}
|
||||
runner_8vcpu_ubuntu: ${{ steps.runner_labels.outputs.runner_8vcpu_ubuntu }}
|
||||
runner_16vcpu_ubuntu: ${{ steps.runner_labels.outputs.runner_16vcpu_ubuntu }}
|
||||
runner_16vcpu_windows: ${{ steps.runner_labels.outputs.runner_16vcpu_windows }}
|
||||
runner_6vcpu_macos: ${{ steps.runner_labels.outputs.runner_6vcpu_macos }}
|
||||
runner_12vcpu_macos: ${{ steps.runner_labels.outputs.runner_12vcpu_macos }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -139,7 +130,6 @@ jobs:
|
||||
OPENCLAW_CI_RUN_NODE_FAST_CI_ROUTING: ${{ github.event_name == 'workflow_dispatch' && 'false' || steps.changed_scope.outputs.run_node_fast_ci_routing || 'false' }}
|
||||
OPENCLAW_CI_RUN_SKILLS_PYTHON: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_skills_python || 'false' }}
|
||||
OPENCLAW_CI_RUN_CONTROL_UI_I18N: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_control_ui_i18n || 'false' }}
|
||||
OPENCLAW_CI_RUN_PROMPT_SNAPSHOTS: ${{ github.event_name == 'workflow_dispatch' && 'true' || steps.changed_scope.outputs.run_prompt_snapshots || 'false' }}
|
||||
OPENCLAW_CI_CHECKOUT_REVISION: ${{ steps.checkout_ref.outputs.sha }}
|
||||
OPENCLAW_CI_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
@@ -204,46 +194,6 @@ jobs:
|
||||
const runSkillsPython = parseBoolean(process.env.OPENCLAW_CI_RUN_SKILLS_PYTHON) && !docsOnly;
|
||||
const runControlUiI18n =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_CONTROL_UI_I18N) && !docsOnly;
|
||||
const runPromptSnapshots =
|
||||
parseBoolean(process.env.OPENCLAW_CI_RUN_PROMPT_SNAPSHOTS) && !docsOnly;
|
||||
const additionalCheckTasks = [
|
||||
{
|
||||
check_name: "check-additional-boundaries-a",
|
||||
group: "boundaries",
|
||||
boundary_shard: "1/4",
|
||||
},
|
||||
{
|
||||
check_name: "check-additional-boundaries-b",
|
||||
group: "boundaries",
|
||||
boundary_shard: "2/4",
|
||||
},
|
||||
{
|
||||
check_name: "check-additional-boundaries-c",
|
||||
group: "boundaries",
|
||||
boundary_shard: "3/4",
|
||||
},
|
||||
{
|
||||
check_name: "check-additional-boundaries-d",
|
||||
group: "boundaries",
|
||||
boundary_shard: "4/4",
|
||||
},
|
||||
{ check_name: "check-additional-extension-channels", group: "extension-channels" },
|
||||
{ check_name: "check-additional-extension-bundled", group: "extension-bundled" },
|
||||
{
|
||||
check_name: "check-additional-extension-package-boundary",
|
||||
group: "extension-package-boundary",
|
||||
},
|
||||
{
|
||||
check_name: "check-additional-runtime-topology-architecture",
|
||||
group: "runtime-topology-architecture",
|
||||
},
|
||||
];
|
||||
if (runPromptSnapshots) {
|
||||
additionalCheckTasks.push({
|
||||
check_name: "check-additional-prompt-snapshots",
|
||||
group: "prompt-snapshots",
|
||||
});
|
||||
}
|
||||
const checksFastCoreTasks = [];
|
||||
if (runNodeFull) {
|
||||
checksFastCoreTasks.push(
|
||||
@@ -309,11 +259,9 @@ jobs:
|
||||
checks_node_core_dist_matrix: createMatrix(nodeTestDistShards),
|
||||
run_check: runNodeFull,
|
||||
run_check_additional: runNodeFull,
|
||||
additional_matrix: createMatrix(runNodeFull ? additionalCheckTasks : []),
|
||||
run_build_smoke: runNodeFull,
|
||||
run_check_docs: docsChanged,
|
||||
run_control_ui_i18n: runControlUiI18n,
|
||||
run_prompt_snapshots: runPromptSnapshots,
|
||||
run_skills_python_job: runSkillsPython,
|
||||
run_checks_windows: runWindows,
|
||||
checks_windows_matrix: createMatrix(
|
||||
@@ -347,13 +295,6 @@ jobs:
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Select runner labels
|
||||
id: runner_labels
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
OPENCLAW_CI_BLACKSMITH_FALLBACK: "true"
|
||||
run: node scripts/ci-runner-labels.mjs
|
||||
|
||||
# Run the fast security/SCM checks in parallel with scope detection so the
|
||||
# main Node jobs do not have to wait for Python/pre-commit setup.
|
||||
security-scm-fast:
|
||||
@@ -511,7 +452,7 @@ jobs:
|
||||
contents: read
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_8vcpu_ubuntu || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
outputs:
|
||||
channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }}
|
||||
@@ -606,11 +547,13 @@ jobs:
|
||||
path: dist-runtime-build.tar.zst
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload A2UI bundle artifact
|
||||
- name: Upload bundled plugin asset artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: canvas-a2ui-bundle
|
||||
path: src/canvas-host/a2ui/
|
||||
name: bundled-plugin-assets
|
||||
path: |
|
||||
extensions/*/src/host/**/.bundle.hash
|
||||
extensions/*/src/host/**/*.bundle.js
|
||||
include-hidden-files: true
|
||||
retention-days: 1
|
||||
|
||||
@@ -633,7 +576,6 @@ jobs:
|
||||
RUN_CHANNELS: ${{ needs.preflight.outputs.run_checks }}
|
||||
RUN_CORE_SUPPORT_BOUNDARY: ${{ needs.preflight.outputs.run_checks_node_core_dist }}
|
||||
RUN_GATEWAY_WATCH: ${{ needs.preflight.outputs.run_check_additional }}
|
||||
OPENCLAW_RUN_PROMPT_SNAPSHOTS: ${{ needs.preflight.outputs.run_prompt_snapshots }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -uo pipefail
|
||||
@@ -711,7 +653,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast_core == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_4vcpu_ubuntu || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -800,67 +742,13 @@ jobs:
|
||||
;;
|
||||
esac
|
||||
|
||||
ci-timings-summary:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
name: ci-timings-summary
|
||||
needs:
|
||||
- preflight
|
||||
- security-fast
|
||||
- build-artifacts
|
||||
- checks-fast-core
|
||||
- checks-fast-plugin-contracts
|
||||
- checks-fast-channel-contracts
|
||||
- checks-fast-protocol
|
||||
- checks
|
||||
- checks-node-compat
|
||||
- checks-node-core-test
|
||||
- check
|
||||
- check-additional
|
||||
- build-smoke
|
||||
- check-docs
|
||||
- skills-python
|
||||
- checks-windows
|
||||
- macos-node
|
||||
- macos-swift
|
||||
- android
|
||||
if: ${{ !cancelled() && always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ needs.preflight.outputs.checkout_revision || github.sha }}
|
||||
fetch-depth: 1
|
||||
fetch-tags: false
|
||||
persist-credentials: false
|
||||
submodules: false
|
||||
|
||||
- name: Write CI timing summary
|
||||
env:
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
node scripts/ci-run-timings.mjs "$RUN_ID" --limit 25 > ci-timings-summary.txt
|
||||
cat ci-timings-summary.txt >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload CI timing summary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ci-timings-summary
|
||||
path: ci-timings-summary.txt
|
||||
retention-days: 14
|
||||
|
||||
checks-fast-plugin-contracts-shard:
|
||||
permissions:
|
||||
contents: read
|
||||
name: ${{ matrix.checkName }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_plugin_contracts_shards == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_4vcpu_ubuntu || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -966,7 +854,7 @@ jobs:
|
||||
name: ${{ matrix.checkName }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_fast == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_4vcpu_ubuntu || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1169,7 +1057,7 @@ jobs:
|
||||
name: checks-node-compat-node22
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_build_artifacts == 'true' && github.event_name == 'workflow_dispatch'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_4vcpu_ubuntu || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -1246,7 +1134,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
|
||||
runs-on: ${{ github.repository != 'openclaw/openclaw' && 'ubuntu-24.04' || matrix.runner == 'blacksmith-4vcpu-ubuntu-2404' && needs.preflight.outputs.runner_4vcpu_ubuntu || matrix.runner == 'blacksmith-8vcpu-ubuntu-2404' && needs.preflight.outputs.runner_8vcpu_ubuntu || matrix.runner == 'blacksmith-16vcpu-ubuntu-2404' && needs.preflight.outputs.runner_16vcpu_ubuntu || matrix.runner || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && (matrix.runner || 'ubuntu-24.04') || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1414,7 +1302,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }}
|
||||
runs-on: ${{ github.repository != 'openclaw/openclaw' && 'ubuntu-24.04' || matrix.runner == 'blacksmith-4vcpu-ubuntu-2404' && needs.preflight.outputs.runner_4vcpu_ubuntu || matrix.runner == 'blacksmith-8vcpu-ubuntu-2404' && needs.preflight.outputs.runner_8vcpu_ubuntu || matrix.runner == 'blacksmith-16vcpu-ubuntu-2404' && needs.preflight.outputs.runner_16vcpu_ubuntu || matrix.runner || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && matrix.runner || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -1575,11 +1463,32 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_8vcpu_ubuntu || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.preflight.outputs.additional_matrix) }}
|
||||
matrix:
|
||||
include:
|
||||
- check_name: check-additional-boundaries-a
|
||||
group: boundaries
|
||||
boundary_shard: 1/4
|
||||
- check_name: check-additional-boundaries-b
|
||||
group: boundaries
|
||||
boundary_shard: 2/4
|
||||
- check_name: check-additional-boundaries-c
|
||||
group: boundaries
|
||||
boundary_shard: 3/4
|
||||
- check_name: check-additional-boundaries-d
|
||||
group: boundaries
|
||||
boundary_shard: 4/4
|
||||
- check_name: check-additional-extension-channels
|
||||
group: extension-channels
|
||||
- check_name: check-additional-extension-bundled
|
||||
group: extension-bundled
|
||||
- check_name: check-additional-extension-package-boundary
|
||||
group: extension-package-boundary
|
||||
- check_name: check-additional-runtime-topology-architecture
|
||||
group: runtime-topology-architecture
|
||||
steps:
|
||||
- name: Checkout
|
||||
shell: bash
|
||||
@@ -1677,7 +1586,6 @@ jobs:
|
||||
env:
|
||||
ADDITIONAL_CHECK_GROUP: ${{ matrix.group }}
|
||||
OPENCLAW_ADDITIONAL_BOUNDARY_SHARD: ${{ matrix.boundary_shard || '' }}
|
||||
OPENCLAW_RUN_PROMPT_SNAPSHOTS: ${{ needs.preflight.outputs.run_prompt_snapshots }}
|
||||
RUN_CONTROL_UI_I18N: ${{ needs.preflight.outputs.run_control_ui_i18n }}
|
||||
OPENCLAW_ADDITIONAL_BOUNDARY_CONCURRENCY: 4
|
||||
OPENCLAW_EXTENSION_BOUNDARY_CONCURRENCY: 6
|
||||
@@ -1705,9 +1613,6 @@ jobs:
|
||||
boundaries)
|
||||
node scripts/run-additional-boundary-checks.mjs
|
||||
;;
|
||||
prompt-snapshots)
|
||||
run_check "prompt:snapshots:check" pnpm prompt:snapshots:check
|
||||
;;
|
||||
extension-channels)
|
||||
run_check "lint:extensions:channels" pnpm run lint:extensions:channels
|
||||
;;
|
||||
@@ -1837,7 +1742,17 @@ jobs:
|
||||
with:
|
||||
install-bun: "false"
|
||||
|
||||
- name: Checkout ClawHub docs source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openclaw/clawhub
|
||||
path: clawhub-source
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check docs
|
||||
env:
|
||||
OPENCLAW_DOCS_SYNC_CLAWHUB_REPO: ${{ github.workspace }}/clawhub-source
|
||||
run: pnpm check:docs
|
||||
|
||||
skills-python:
|
||||
@@ -1877,7 +1792,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_checks_windows == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_16vcpu_windows || 'windows-2025' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-windows-2025' || 'windows-2025' }}
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
@@ -1990,7 +1905,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_macos_node == 'true' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_6vcpu_macos || 'macos-latest' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-6vcpu-macos-latest' || 'macos-latest' }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -2034,7 +1949,7 @@ jobs:
|
||||
name: "macos-swift"
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_macos_swift == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_12vcpu_macos || 'macos-latest' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -2131,7 +2046,7 @@ jobs:
|
||||
name: ${{ matrix.check_name }}
|
||||
needs: [preflight]
|
||||
if: needs.preflight.outputs.run_android_job == 'true'
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && needs.preflight.outputs.runner_8vcpu_ubuntu || 'ubuntu-24.04' }}
|
||||
runs-on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04' }}
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
75
.github/workflows/codeql-critical-quality.yml
vendored
75
.github/workflows/codeql-critical-quality.yml
vendored
@@ -21,17 +21,21 @@ on:
|
||||
- plugin-sdk-package-contract
|
||||
- plugin-sdk-reply-runtime
|
||||
- provider-runtime-boundary
|
||||
- network-runtime-boundary
|
||||
- session-diagnostics-boundary
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- ".github/codeql/**"
|
||||
- ".github/workflows/codeql-critical-quality.yml"
|
||||
- "extensions/*.ts"
|
||||
- "extensions/**/*.ts"
|
||||
- "packages/plugin-package-contract/**"
|
||||
- "packages/plugin-sdk/**"
|
||||
- "packages/memory-host-sdk/**"
|
||||
- "src/*.ts"
|
||||
- "src/**/*.ts"
|
||||
- "src/config/**"
|
||||
- "extensions/bluebubbles/src/**"
|
||||
- "extensions/discord/src/**"
|
||||
- "extensions/feishu/src/**"
|
||||
- "extensions/googlechat/src/**"
|
||||
@@ -159,6 +163,7 @@ jobs:
|
||||
plugin_sdk_package: ${{ steps.detect.outputs.plugin_sdk_package }}
|
||||
plugin_sdk_reply: ${{ steps.detect.outputs.plugin_sdk_reply }}
|
||||
provider: ${{ steps.detect.outputs.provider }}
|
||||
network_runtime: ${{ steps.detect.outputs.network_runtime }}
|
||||
session_diagnostics: ${{ steps.detect.outputs.session_diagnostics }}
|
||||
steps:
|
||||
- name: Detect PR shard paths
|
||||
@@ -182,6 +187,7 @@ jobs:
|
||||
plugin_sdk_package=false
|
||||
plugin_sdk_reply=false
|
||||
provider=false
|
||||
network_runtime=false
|
||||
session_diagnostics=false
|
||||
|
||||
if [[ "${EVENT_NAME}" != "pull_request" ]]; then
|
||||
@@ -196,6 +202,7 @@ jobs:
|
||||
plugin_sdk_package=true
|
||||
plugin_sdk_reply=true
|
||||
provider=true
|
||||
network_runtime=true
|
||||
session_diagnostics=true
|
||||
else
|
||||
while IFS= read -r file; do
|
||||
@@ -212,6 +219,7 @@ jobs:
|
||||
plugin_sdk_package=true
|
||||
plugin_sdk_reply=true
|
||||
provider=true
|
||||
network_runtime=true
|
||||
session_diagnostics=true
|
||||
;;
|
||||
src/acp/control-plane/*|src/agents/cli-runner/*|src/agents/command/*|src/agents/pi-embedded-runner/*|src/agents/tools/*|src/agents/*completion*.ts|src/agents/*transport*.ts|src/agents/model-*.ts|src/agents/openclaw-tools*.ts|src/agents/provider-*.ts|src/agents/session*.ts|src/agents/tool-call*.ts|src/auto-reply/reply/agent-runner*.ts|src/auto-reply/reply/commands*.ts|src/auto-reply/reply/directive-handling*.ts|src/auto-reply/reply/dispatch-*.ts|src/auto-reply/reply/get-reply-run*.ts|src/auto-reply/reply/provider-dispatcher*.ts|src/auto-reply/reply/queue*.ts|src/auto-reply/reply/reply-run-registry*.ts|src/auto-reply/reply/session*.ts)
|
||||
@@ -220,7 +228,7 @@ jobs:
|
||||
src/auto-reply/reply/post-compaction-context.ts|src/auto-reply/reply/queue/*|src/auto-reply/reply/startup-context.ts|src/commands/doctor-session-*.ts|src/commands/session-store-targets.ts|src/commands/sessions*.ts|src/infra/diagnostic-*.ts|src/infra/diagnostics-timeline.ts|src/infra/session-delivery-queue*.ts|src/logging/diagnostic*.ts)
|
||||
session_diagnostics=true
|
||||
;;
|
||||
extensions/bluebubbles/src/*|extensions/discord/src/*|extensions/feishu/src/*|extensions/googlechat/src/*|extensions/imessage/src/*|extensions/irc/src/*|extensions/line/src/*|extensions/matrix/src/*|extensions/mattermost/src/*|extensions/msteams/src/*|extensions/nextcloud-talk/src/*|extensions/nostr/src/*|extensions/qa-channel/src/*|extensions/qqbot/src/*|extensions/signal/src/*|extensions/slack/src/*|extensions/synology-chat/src/*|extensions/telegram/src/*|extensions/tlon/src/*|extensions/twitch/src/*|extensions/whatsapp/src/*|extensions/zalo/src/*|extensions/zalouser/src/*|src/channels/*)
|
||||
extensions/discord/src/*|extensions/feishu/src/*|extensions/googlechat/src/*|extensions/imessage/src/*|extensions/irc/src/*|extensions/line/src/*|extensions/matrix/src/*|extensions/mattermost/src/*|extensions/msteams/src/*|extensions/nextcloud-talk/src/*|extensions/nostr/src/*|extensions/qa-channel/src/*|extensions/qqbot/src/*|extensions/signal/src/*|extensions/slack/src/*|extensions/synology-chat/src/*|extensions/telegram/src/*|extensions/tlon/src/*|extensions/twitch/src/*|extensions/whatsapp/src/*|extensions/zalo/src/*|extensions/zalouser/src/*|src/channels/*)
|
||||
channel=true
|
||||
;;
|
||||
src/config/*)
|
||||
@@ -281,6 +289,12 @@ jobs:
|
||||
plugin_sdk_package=true
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${file}" in
|
||||
src/*.ts|src/**/*.ts|extensions/*.ts|extensions/**/*.ts)
|
||||
network_runtime=true
|
||||
;;
|
||||
esac
|
||||
done < <(gh api --paginate "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[].filename')
|
||||
fi
|
||||
|
||||
@@ -296,6 +310,7 @@ jobs:
|
||||
echo "plugin_sdk_package=${plugin_sdk_package}"
|
||||
echo "plugin_sdk_reply=${plugin_sdk_reply}"
|
||||
echo "provider=${provider}"
|
||||
echo "network_runtime=${network_runtime}"
|
||||
echo "session_diagnostics=${session_diagnostics}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
@@ -391,6 +406,62 @@ jobs:
|
||||
with:
|
||||
category: "/codeql-critical-quality/channel-runtime-boundary"
|
||||
|
||||
network-runtime-boundary:
|
||||
name: Critical Quality (network-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.network_runtime == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'network-runtime-boundary') }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
config-file: ./.github/codeql/codeql-network-runtime-boundary-critical-quality.yml
|
||||
|
||||
- name: Analyze
|
||||
id: analyze
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||
with:
|
||||
output: sarif-results
|
||||
category: "/codeql-critical-quality/network-runtime-boundary"
|
||||
|
||||
- name: Fail on network runtime boundary findings
|
||||
env:
|
||||
SARIF_OUTPUT: sarif-results
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
files=("$SARIF_OUTPUT"/*.sarif)
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "No SARIF files found in $SARIF_OUTPUT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
findings="$(jq -s '[.[].runs[]?.results[]?] | length' "${files[@]}")"
|
||||
if [ "$findings" = "0" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found ${findings} network runtime boundary finding(s):" >&2
|
||||
jq -r '
|
||||
.runs[]?.results[]?
|
||||
| .locations[0].physicalLocation as $location
|
||||
| "- "
|
||||
+ ($location.artifactLocation.uri // "unknown")
|
||||
+ ":"
|
||||
+ (($location.region.startLine // 0) | tostring)
|
||||
+ " "
|
||||
+ (.message.text // .ruleId)
|
||||
' "${files[@]}" >&2
|
||||
exit 1
|
||||
|
||||
agent-runtime-boundary:
|
||||
name: Critical Quality (agent-runtime-boundary)
|
||||
needs: quality-shards
|
||||
|
||||
16
.github/workflows/docs-sync-publish.yml
vendored
16
.github/workflows/docs-sync-publish.yml
vendored
@@ -22,6 +22,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout ClawHub docs source
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: openclaw/clawhub
|
||||
path: clawhub-source
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
token: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN || github.token }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -48,12 +57,17 @@ jobs:
|
||||
|
||||
- name: Sync docs into publish repo
|
||||
run: |
|
||||
clawhub_sha="$(git -C "$GITHUB_WORKSPACE/clawhub-source" rev-parse HEAD)"
|
||||
node scripts/docs-sync-publish.mjs \
|
||||
--target "$GITHUB_WORKSPACE/publish" \
|
||||
--source-repo "$GITHUB_REPOSITORY" \
|
||||
--source-sha "$GITHUB_SHA"
|
||||
--source-sha "$GITHUB_SHA" \
|
||||
--clawhub-repo "$GITHUB_WORKSPACE/clawhub-source" \
|
||||
--clawhub-source-repo "openclaw/clawhub" \
|
||||
--clawhub-source-sha "$clawhub_sha"
|
||||
|
||||
- name: Install docs MDX checker dependency
|
||||
working-directory: publish
|
||||
run: npm install --no-save --package-lock=false @mdx-js/mdx@3.1.1
|
||||
|
||||
- name: Check publish docs MDX
|
||||
|
||||
2
.github/workflows/macos-release.yml
vendored
2
.github/workflows/macos-release.yml
vendored
@@ -98,5 +98,5 @@ jobs:
|
||||
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-validate.yml\` with tag \`${RELEASE_TAG}\` and wait for the private mac validation lane to pass."
|
||||
echo "- Run \`openclaw/releases-private/.github/workflows/openclaw-macos-publish.yml\` with tag \`${RELEASE_TAG}\` and \`preflight_only=true\` for the full private mac preflight."
|
||||
echo "- For the real publish path, run the same private mac publish workflow from \`main\` with the successful private preflight \`preflight_run_id\` so it promotes the prepared artifacts instead of rebuilding them."
|
||||
echo "- For stable releases, also download \`macos-appcast-${RELEASE_TAG}\` from the successful private run and commit \`appcast.xml\` back to \`main\` in \`openclaw/openclaw\`."
|
||||
echo "- For stable releases, the private publish workflow also publishes the signed \`appcast.xml\` to public \`main\`, or opens an appcast PR if direct push is blocked."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
@@ -2135,8 +2135,8 @@ jobs:
|
||||
# inside the already-isolated container to keep MCP cron/tool
|
||||
# execution representative instead of failing on nested sandbox
|
||||
# setup.
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
|
||||
@@ -2354,8 +2354,8 @@ jobs:
|
||||
live-cli-backend-docker)
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_MODEL=codex-cli/gpt-5.4" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key" >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"fast\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo 'OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=["exec","resume","{sessionId}","-c","sandbox_mode=\"danger-full-access\"","-c","service_tier=\"priority\"","--skip-git-repo-check"]' >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_LIVE_CLI_BACKEND_DEBUG=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_CLI_BACKEND_LOG_OUTPUT=1" >> "$GITHUB_ENV"
|
||||
echo "OPENCLAW_TEST_CONSOLE=1" >> "$GITHUB_ENV"
|
||||
|
||||
96
.github/workflows/openclaw-release-publish.yml
vendored
96
.github/workflows/openclaw-release-publish.yml
vendored
@@ -37,10 +37,15 @@ on:
|
||||
required: true
|
||||
default: true
|
||||
type: boolean
|
||||
wait_for_clawhub:
|
||||
description: Wait for ClawHub plugin publish before marking this workflow complete
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: openclaw-release-publish-${{ inputs.tag }}
|
||||
@@ -166,6 +171,7 @@ jobs:
|
||||
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
|
||||
PLUGINS: ${{ inputs.plugins }}
|
||||
PUBLISH_OPENCLAW_NPM: ${{ inputs.publish_openclaw_npm && 'true' || 'false' }}
|
||||
WAIT_FOR_CLAWHUB: ${{ inputs.wait_for_clawhub && 'true' || 'false' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -203,19 +209,31 @@ jobs:
|
||||
fi
|
||||
|
||||
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" >&2
|
||||
{
|
||||
echo "- ${workflow}: dispatched (https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id})"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
printf '%s\n' "${run_id}"
|
||||
}
|
||||
|
||||
wait_for_run() {
|
||||
local workflow="$1"
|
||||
local run_id="$2"
|
||||
local status conclusion url
|
||||
local status conclusion url updated_at last_state
|
||||
|
||||
last_state=""
|
||||
while true; do
|
||||
status="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status --jq '.status')"
|
||||
run_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json status,url,updatedAt)"
|
||||
status="$(printf '%s' "$run_json" | jq -r '.status')"
|
||||
if [[ "$status" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
url="$(printf '%s' "$run_json" | jq -r '.url')"
|
||||
updated_at="$(printf '%s' "$run_json" | jq -r '.updatedAt')"
|
||||
state="${status}:${updated_at}"
|
||||
if [[ "$state" != "$last_state" ]]; then
|
||||
echo "${workflow} still ${status} (updated ${updated_at}): ${url}"
|
||||
last_state="$state"
|
||||
fi
|
||||
sleep 30
|
||||
done
|
||||
|
||||
@@ -245,6 +263,53 @@ jobs:
|
||||
wait_run_pid="$!"
|
||||
}
|
||||
|
||||
create_or_update_github_release() {
|
||||
local release_version notes_version title notes_file changelog_file latest_arg prerelease_args
|
||||
release_version="${RELEASE_TAG#v}"
|
||||
notes_version="${release_version}"
|
||||
if [[ "${notes_version}" =~ ^([0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*)-(alpha|beta)\.[1-9][0-9]*$ ]]; then
|
||||
notes_version="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
title="openclaw ${release_version}"
|
||||
changelog_file="${RUNNER_TEMP}/CHANGELOG.md"
|
||||
notes_file="${RUNNER_TEMP}/release-notes.md"
|
||||
|
||||
gh api --repo "$GITHUB_REPOSITORY" "repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md?ref=${TARGET_SHA}" \
|
||||
--jq '.content' | base64 --decode > "${changelog_file}"
|
||||
awk -v version="${notes_version}" '
|
||||
$0 == "## " version { in_section = 1; next }
|
||||
/^## / && in_section { exit }
|
||||
in_section { print }
|
||||
' "${changelog_file}" > "${notes_file}"
|
||||
if [[ ! -s "${notes_file}" ]]; then
|
||||
echo "CHANGELOG.md does not contain release notes for ${notes_version}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
prerelease_args=()
|
||||
latest_arg="--latest=false"
|
||||
if [[ "${RELEASE_TAG}" == *"-alpha."* || "${RELEASE_TAG}" == *"-beta."* ]]; then
|
||||
prerelease_args=(--prerelease)
|
||||
elif [[ "${RELEASE_NPM_DIST_TAG}" == "latest" ]]; then
|
||||
latest_arg="--latest"
|
||||
fi
|
||||
|
||||
if gh release view "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release edit "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "${title}" \
|
||||
--notes-file "${notes_file}" \
|
||||
"${prerelease_args[@]}"
|
||||
else
|
||||
gh release create "${RELEASE_TAG}" --repo "$GITHUB_REPOSITORY" \
|
||||
--verify-tag \
|
||||
--title "${title}" \
|
||||
--notes-file "${notes_file}" \
|
||||
"${prerelease_args[@]}" \
|
||||
"${latest_arg}"
|
||||
fi
|
||||
echo "- GitHub release: https://github.com/${GITHUB_REPOSITORY}/releases/tag/${RELEASE_TAG}" >> "$GITHUB_STEP_SUMMARY"
|
||||
}
|
||||
|
||||
{
|
||||
echo "### Publish sequence"
|
||||
echo
|
||||
@@ -257,6 +322,11 @@ jobs:
|
||||
else
|
||||
echo "- OpenClaw npm publish: skipped by input"
|
||||
fi
|
||||
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
echo "- Workflow completion waits for ClawHub"
|
||||
else
|
||||
echo "- Workflow completion does not wait for ClawHub; monitor the dispatched ClawHub run separately"
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}")
|
||||
@@ -286,10 +356,16 @@ jobs:
|
||||
echo "- OpenClaw npm publish: skipped by input" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
|
||||
wait_run_pid=""
|
||||
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
|
||||
clawhub_pid="${wait_run_pid}"
|
||||
clawhub_result=""
|
||||
clawhub_pid=""
|
||||
if [[ "${WAIT_FOR_CLAWHUB}" == "true" ]]; then
|
||||
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
|
||||
wait_run_pid=""
|
||||
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${clawhub_result}"
|
||||
clawhub_pid="${wait_run_pid}"
|
||||
else
|
||||
echo "- plugin-clawhub-release.yml: not awaited (${plugin_clawhub_run_id})" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
openclaw_result=""
|
||||
openclaw_pid=""
|
||||
@@ -301,7 +377,7 @@ jobs:
|
||||
fi
|
||||
|
||||
failed=0
|
||||
if ! wait "${clawhub_pid}"; then
|
||||
if [[ -n "${clawhub_pid}" ]] && ! wait "${clawhub_pid}"; then
|
||||
failed=1
|
||||
fi
|
||||
if [[ -n "${openclaw_pid}" ]] && ! wait "${openclaw_pid}"; then
|
||||
@@ -316,3 +392,7 @@ jobs:
|
||||
if [[ "${failed}" != "0" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${openclaw_npm_run_id}" ]]; then
|
||||
create_or_update_github_release
|
||||
fi
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -68,6 +68,8 @@ apps/ios/*.xcfilelist
|
||||
vendor/a2ui/renderers/lit/dist/
|
||||
src/canvas-host/a2ui/*.bundle.js
|
||||
src/canvas-host/a2ui/*.map
|
||||
extensions/canvas/src/host/a2ui/*.bundle.js
|
||||
extensions/canvas/src/host/a2ui/*.map
|
||||
.bundle.hash
|
||||
|
||||
# fastlane (iOS)
|
||||
@@ -220,3 +222,4 @@ extensions/**/.openclaw-runtime-deps-stamp.json
|
||||
# Output dir for scripts/run-opengrep.sh (local opengrep scans)
|
||||
/.opengrep-out/
|
||||
/.crabbox-artifacts
|
||||
.comux*
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"docker-compose.yml",
|
||||
"dist/",
|
||||
"docs/_layouts/",
|
||||
"**/*.json",
|
||||
"node_modules/",
|
||||
"patches/",
|
||||
"pnpm-lock.yaml/",
|
||||
|
||||
@@ -32,10 +32,16 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
- Owner boundary: fix owner-specific behavior in the owner module. Shared/core gets generic seams only; no owner ids, dependency strings, defaults, migrations, or recovery policy. If a bug names an extension or its dependency, start in that extension and add a generic core seam only when multiple owners need it.
|
||||
- Dependency ownership follows runtime ownership: extension-only deps stay plugin-local; root deps only for core imports or intentionally internalized bundled plugin runtime.
|
||||
- Legacy config repair: doctor/fix paths, not startup/load-time core migrations.
|
||||
- No legacy compatibility in core/runtime paths. When old config/store shapes need support, add an `openclaw doctor --fix` rewrite/repair rule with tests and keep runtime code on the canonical contract.
|
||||
- Core test asserting extension-specific behavior: move to owner extension or generic contract test.
|
||||
- New seams: backwards-compatible, documented, versioned. Third-party plugins exist.
|
||||
- Channels: `src/channels/**` is implementation; plugin authors get SDK seams.
|
||||
- Providers: core owns generic loop; provider plugins own auth/catalog/runtime hooks.
|
||||
- Request-time runtime resolution: when a path already knows the provider id, model ref, channel id, outbound target, capability family, or attachment class, carry that as a prepared runtime fact instead of rediscovering it later.
|
||||
- Prepared runtime facts should be small typed values produced once near startup, reply dispatch, model selection, tool planning, or channel resolution, then passed through context to consumers. Prefer `AgentRuntimePlan`, `ProviderRuntimePluginHandle`, scoped model/catalog helpers, active/runtime registries, manifest/public-artifact lookups, single-provider resolvers, and lazy registry construction.
|
||||
- Avoid broad request-time rediscovery: hot reply/tool/outbound/media paths should not call broad plugin/provider/channel/capability loaders such as `loadOpenClawPlugins`, `resolveProviderPluginsForHooks`, `resolvePluginCapabilityProviders`, `resolvePluginDiscoveryProvidersRuntime`, `getChannelPlugin`, or broad model/tool/media registry builders just to answer a question the caller already knows. Do not build multimodal/provider registries for document-only or otherwise non-participating paths.
|
||||
- Compatibility fallbacks are allowed only for startup/setup/admin/standalone/legacy callers that genuinely lack prepared facts. Keep them explicit, tested, and outside migrated hot reply/tool/outbound paths.
|
||||
- Do not fix repeated request-time discovery by adding scattered cache layers. Move the canonical fact earlier, reuse the existing prepared-runtime object, and delete duplicate lookup branches when the last migrated caller stops needing them.
|
||||
- Gateway protocol changes: additive first; incompatible needs versioning/docs/client follow-through.
|
||||
- Config contract: exported types, schema/help, metadata, baselines, docs aligned. Retired public keys stay retired; compat in raw migration/doctor.
|
||||
- Direction: manifest-first control plane; targeted runtime loaders; no hidden contract bypasses; broad mutable registries transitional.
|
||||
@@ -189,7 +195,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
- Mac gateway: dev watch = `pnpm gateway:watch` (tmux `openclaw-gateway-watch-main`, auto-attach). Noninteractive: `OPENCLAW_GATEWAY_WATCH_ATTACH=0 pnpm gateway:watch`; attach/stop: `tmux attach -t openclaw-gateway-watch-main` / `tmux kill-session -t openclaw-gateway-watch-main`. Managed installs: `openclaw gateway restart/status --deep`. No launchd/ad-hoc tmux. Logs: `./scripts/clawlog.sh`.
|
||||
- Version bump touches: `package.json`, `apps/android/app/build.gradle.kts`, `apps/ios/version.json` + `pnpm ios:version:sync`, macOS `Info.plist`, `docs/install/updating.md`. Appcast only for Sparkle release.
|
||||
- Mobile LAN pairing: plaintext `ws://` loopback-only. Private-network `ws://` needs `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1`; Tailscale/public use `wss://` or tunnel.
|
||||
- A2UI hash `src/canvas-host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately.
|
||||
- A2UI hash `extensions/canvas/src/host/a2ui/.bundle.hash`: generated; ignore unless running `pnpm canvas:a2ui:bundle`; commit separately.
|
||||
|
||||
## Ops / Footguns
|
||||
|
||||
|
||||
105
CHANGELOG.md
105
CHANGELOG.md
@@ -6,21 +6,40 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Agents/failover: harden state-aware lane suspension by persisting quota resume transitions, restoring configured lane concurrency, preserving non-quota failure reasons, and exporting model failover events through diagnostics OTLP. Thanks @BunsDev.
|
||||
- Channels/streaming: make progress draft labels scroll away with other progress lines, render structured tool rows as compact emoji/title/details, show web-search queries from provider-native argument shapes, and skip empty Discord apply-patch starts until a patch summary exists. (#79146)
|
||||
- Telegram: preserve the channel-specific 10-option poll cap in the unified outbound adapter so over-limit polls are rejected before send. (#78762) Thanks @obviyus.
|
||||
- Runtime/install: raise the supported Node 22 floor to `22.16+` so native SQLite query handling can rely on the `node:sqlite` statement metadata API while continuing to recommend Node 24. (#78921)
|
||||
- Discord/voice: include a bounded one-line STT transcript preview in verbose voice logs so live voice debugging shows what speakers said before the agent reply.
|
||||
- Codex app-server: pin the managed Codex harness and Codex CLI smoke package to `@openai/codex@0.129.0`, defer OpenClaw integration dynamic tools behind Codex tool search by default, and accept current Codex service-tier values so legacy `fast` settings survive the stable harness upgrade as `priority`.
|
||||
- Discord/voice: stream ElevenLabs TTS directly into Discord playback and send ElevenLabs latency optimization as the documented query parameter so spoken replies can start sooner.
|
||||
- Discord/voice: keep TTS playback running when another user starts speaking, ignore new capture during playback to avoid feedback loops, and downgrade expected receive-stream aborts to verbose diagnostics.
|
||||
- Telegram: treat successful same-chat `message` tool outbound sends during an inbound telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback (#78685). Thanks @neeravmakwana.
|
||||
- Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared even when a child session row remains, and apply the default bounded reload deferral timeout to channel hot reloads so stale task records cannot block Discord/Slack/Telegram reloads forever.
|
||||
- Gateway/sessions: keep session-store index writes atomic while skipping durable fsync inside the writer lock, reducing cron and channel-turn starvation on slow filesystems and addressing the session-store strand of #73655. Thanks @mmartoccia.
|
||||
- Discord/voice: make `openclaw channels capabilities --channel discord --target channel:<id>` and `channels status --probe` audit voice-channel permissions, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before `/vc join`.
|
||||
- Docs/iMessage: deprecate BlueBubbles for new OpenClaw setups, document the upstream server-release rationale, and point new iMessage deployments toward the native `imsg` path while keeping BlueBubbles as a supported legacy fallback.
|
||||
- Channels CLI: make `openclaw channels list` channel-only — drop the `Auth providers (OAuth + API keys)` block (use `openclaw models auth list`), drop the per-provider usage/quota fetch and the `--no-usage` flag (use `openclaw status` or `openclaw models list`), add `--all` to surface bundled-unconfigured, catalog-not-installed, and catalog-installed-but-unconfigured channels, and render explicit `installed` / `configured` / `enabled` tags per row plus an `origin` + `installed` field in JSON. Fixes WeCom-class catalog channels disappearing from `--all` when installed on disk but not yet configured. (#78456) Thanks @sliverp.
|
||||
- CLI/cron: add computed `status` field to `cron list --json` and `cron show <id> --json` output, mirroring the human-readable status column (disabled/running/ok/error/skipped/idle) so external tooling can determine job state without re-deriving it from raw state fields. (#78701) Thanks @aweiker.
|
||||
- Discord/voice: make voice capture less choppy by extending the default post-speech silence grace to 2.5s, add `voice.captureSilenceGraceMs` for noisy Discord sessions, and tighten the spoken-output prompt around live STT fragments. Thanks @vincentkoc.
|
||||
- Discord/streaming: default Discord replies to progress draft previews so tool/work activity appears in one edited Discord message unless `channels.discord.streaming.mode` is set to `off`.
|
||||
- OpenAI: support `openai/chat-latest` as an explicit direct API-key model override for trying the moving ChatGPT Instant API alias without changing the stable default model.
|
||||
- OpenAI/realtime: default realtime voice to `gpt-realtime-2`, use the GA Realtime WebSocket session shape for backend OpenAI bridges, and cover backend, WebRTC, Google Live, and Gateway relay paths in the live Talk smoke. (#79130)
|
||||
- Plugins/install: add `npm-pack:<path.tgz>` installs so local npm pack artifacts run through the same managed npm-root install, lockfile verification, dependency scan, and install-record path as registry npm plugins.
|
||||
- Channels/plugins: show configured official external channels as missing-plugin status rows and send errors with exact install/doctor repair commands after raw package-manager upgrades leave Feishu or WhatsApp uninstalled. Fixes #78702 and #78593. Thanks @MarkMa84 and @mkupiainen.
|
||||
- Codex app-server: disarm the short post-tool completion watchdog after current-turn activity, expose `appServer.turnCompletionIdleTimeoutMs`, and include raw assistant item context in idle-timeout diagnostics so status-only post-tool stalls stop failing as idle. Fixes #77984. Thanks @roseware-dev and @rubencu.
|
||||
- Plugin skills/Windows: publish plugin-provided skill directories as junctions on Windows so standard users without Developer Mode can register plugin skills without symlink EPERM failures. Fixes #77958. (#77971) Thanks @hclsys and @jarro.
|
||||
- MS Teams: surface blocked Bot Framework egress by logging JWKS fetch network failures and adding a Bot Connector send hint for transport-level reply failures. Fixes #77674. (#78081) Thanks @Beandon13.
|
||||
- Gateway/sessions: fast-path already-qualified model refs while building session-list rows so `openclaw sessions` and Control UI session lists avoid heavyweight model resolution on large stores. (#77902) Thanks @ragesaq.
|
||||
- Contributor PRs: remind external contributors to redact private information like IP addresses, API keys, phone numbers, and non-public endpoints from real behavior proof. Thanks @pashpashpash.
|
||||
- Codex/approvals: in Codex approval modes, stop installing the pre-guardian native `PermissionRequest` hook by default so Codex's reviewer can approve safe commands before OpenClaw surfaces an approval, remember `allow-always` decisions for identical Codex native `PermissionRequest` payloads within the active session window, and make plugin approval requests validate/render their actual allowed decisions so Telegram and other native approval UIs cannot offer stale actions. Thanks @shakkernerd.
|
||||
- Codex/plugins: enable migrated source-installed `openai-curated` Codex plugins in the same Codex harness thread with explicit `codexPlugins` config, cached app readiness, and fail-closed destructive-action policy. Thanks @kevinslin.
|
||||
- Codex/plugins: enforce native plugin destructive-action policy with Codex app-level `destructive_enabled` config instead of OpenClaw-maintained per-tool deny lists, leave plugin app `open_world_enabled` on by default, and invalidate existing plugin app thread bindings so old generated app config is rebuilt. Thanks @kevinslin.
|
||||
- PR triage: mark external pull requests with `proof: supplied` when Barnacle finds structured real behavior proof, keep stale negative proof labels in sync across CRLF-edited PR bodies, and let ClawSweeper own the stronger `proof: sufficient` judgement.
|
||||
- Sessions CLI: show the selected agent runtime in the `openclaw sessions` table so terminal output matches the runtime visibility already present in JSON/status surfaces. Thanks @vincentkoc.
|
||||
- ACPX/Codex: preserve trusted Codex project declarations when launching isolated Codex ACP sessions, avoiding interactive trust prompts in headless runs. Thanks @Stedyclaw.
|
||||
- ACPX/Codex: reap stale OpenClaw-owned ACPX/Codex ACP process trees on startup and after ACP session close, preventing orphaned harness processes from slowing the Gateway. Thanks @91wan.
|
||||
- ACP bridge: implement stable session list, resume, and close handlers so ACP clients can page Gateway sessions, rebind existing sessions without replay, and close bridge sessions cleanly. Thanks @amknight.
|
||||
- ACP sessions: allow parent agents to inspect and message their own spawned cross-agent ACP sessions without enabling broad agent-to-agent visibility. Thanks @barronlroth.
|
||||
- Talk/voice: unify realtime relay, transcription relay, managed-room handoff, Voice Call, Google Meet, VoiceClaw, and native clients around a shared Talk session controller and add the Gateway-managed `talk.session.*` RPC surface.
|
||||
- Diagnostics/Talk: export bounded Talk lifecycle/audio metrics and session recovery metrics through OpenTelemetry and Prometheus without exposing transcripts, audio payloads, room ids, turn ids, or session ids.
|
||||
- Logging/Talk: route shared Talk lifecycle events into bounded file and OTLP log records while keeping transcript text, audio payloads, turn ids, call ids, and provider item ids out of logs.
|
||||
@@ -43,19 +62,24 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/Windows: bind the default loopback gateway listener only to `127.0.0.1` on Windows so libuv's dual-stack `::1` behavior cannot wedge localhost HTTP requests. (#69701, fixes #69674) Thanks @SARAMALI15792.
|
||||
- Slack/streaming: add `streaming.progress.render: "rich"` for Block Kit progress drafts backed by structured progress line data.
|
||||
- Slack/streaming: keep the newest rich progress lines when Block Kit limits trim long progress drafts. Thanks @vincentkoc.
|
||||
- Slack/performance: reduce message preparation, stream recipient lookup, and thread-context allocation overhead on Slack reply hot paths. Thanks @vincentkoc.
|
||||
- Channels/streaming: cap progress-draft tool lines by default so edited progress boxes avoid jumpy reflow from long wrapped lines.
|
||||
- Control UI/chat: add an agent-first filter to the chat session picker, keep chat controls/composer responsive across phone/tablet/desktop widths, keep desktop chat controls on one row, avoid duplicate avatar refreshes during initial chat load, and hide that row while scrolling down the transcript. Thanks @BunsDev.
|
||||
- Control UI/chat: collapse consecutive duplicate text messages into one bubble with a count so repeated text-only messages stay compact without hiding nearby context.
|
||||
- Control UI/chat and Sessions: label inherited thinking defaults separately from explicit overrides while preserving provider-supplied option labels. Fixes #77581. Thanks @BunsDev and @Beandon13.
|
||||
- Agents/runtime: add prepared runtime foundation contracts for carrying provider, model, tool, TTS, and outbound runtime facts through later reply-path migrations. Thanks @mcaxtr.
|
||||
- Control UI/WhatsApp: keep Show QR available for unlinked WhatsApp accounts while switching linked accounts to the explicit Relink action and showing Wait for scan only when a QR is active. Thanks @BunsDev.
|
||||
- Agents/subagents: preserve every grouped child result when direct completion fallback has to bypass the requester-agent announce turn. Thanks @vincentkoc.
|
||||
- TTS/telephony: honor provider voice/model overrides in telephony synthesis providers so Google Meet agent speech logs match the backend that actually produced the audio. Thanks @vincentkoc.
|
||||
- Voice Call/realtime: bound the paced Twilio audio queue and close overloaded realtime streams before provider audio can pile up behind the websocket backpressure guard. Thanks @vincentkoc.
|
||||
- Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi.
|
||||
- Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup.
|
||||
- Gateway/performance: reuse the compatible plugin metadata snapshot across dashboard and channel agent turns so auto-enabled runtime config does not repeatedly rescan plugin metadata before provider calls. Thanks @shakkernerd.
|
||||
- Gateway/performance: reuse current plugin metadata for provider activation, auth/env candidate lookup, and bundle settings during dashboard and channel agent turns while keeping the configless secret-target cache unscoped and refusing stale unscoped reuse when plugin discovery roots differ. Thanks @shakkernerd.
|
||||
- Gateway/performance: avoid resolving plugin auto-enable metadata twice in one runtime config pass, reducing repeated dashboard turn metadata scans. Thanks @shakkernerd.
|
||||
- Auth/providers: pass `config` and `workspaceDir` lookup context through to provider-id resolution so workspace-scoped auth aliases resolve correctly when no explicit alias map is supplied. Thanks @shakkernerd.
|
||||
- Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed.
|
||||
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and default sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
|
||||
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and opt-in sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
|
||||
- Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc.
|
||||
- QA/Mantis: add `pnpm openclaw qa mantis slack-desktop-smoke` to run Slack live QA inside a Crabbox VNC desktop, open Slack Web, and capture desktop screenshots beside the Slack QA artifacts.
|
||||
- QA/Mantis: add an opt-in Discord thread attachment before/after scenario that creates a real thread, calls `message.thread-reply` with `filePath`, and captures baseline/candidate screenshot evidence.
|
||||
@@ -81,6 +105,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/update: repair stale managed npm-root `openclaw` peer packages before plugin installs, so beta-channel official plugin updates are not downgraded by old core package-lock state. Thanks @vincentkoc.
|
||||
- Plugins/install: run managed npm-root install, rollback, repair, and uninstall mutations with legacy peer resolution so removing one plugin cannot rehydrate a stale registry `openclaw` package into the shared root. Thanks @vincentkoc.
|
||||
- Plugins/install: reassert managed npm plugin `openclaw` peer links after shared-root npm installs, updates, and uninstalls, so mutating one plugin does not leave previously installed SDK-using plugins unable to resolve `openclaw/plugin-sdk/*`.
|
||||
- Plugins/install: use the same absolute POSIX npm lifecycle shell for managed plugin install, rollback, repair, and uninstall npm operations as staged package updates, preventing restricted PATH shells from breaking cleanup. Thanks @vincentkoc.
|
||||
- Plugins/update: make package upgrades swap pnpm/npm-prefix installs cleanly, keep legacy plugin install runtime chunks working, and on the beta channel fall back default-line npm plugins to default/latest when plugin beta releases are missing or fail install validation. Thanks @vincentkoc and @joshavant.
|
||||
- Plugins/active-memory: skip session-store channel entries that contain `:` when resolving the recall subagent's channel, so QQ c2c agent IDs (e.g. `c2c:10D4F7C2…`) and other scoped conversation IDs do not reach bundled-plugin `dirName` validation and crash the recall run. The same guard already applied to explicit `channelId` params (#76704); this extends it to store-derived channels. (#77396) Thanks @hclsys.
|
||||
- Sandbox/Windows: accept drive-absolute Docker bind sources while keeping sandbox blocked-path and allowed-root policy comparisons Windows-case-insensitive. (#42174) Thanks @6607changchun.
|
||||
@@ -97,16 +122,19 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed.
|
||||
- Plugins/loader: preserve real compiled plugin module evaluation errors on the native fast path instead of treating every thrown `.js` module as a source-transform fallback miss. Thanks @vincentkoc.
|
||||
- Plugin SDK/fs-safe: expose reusable atomic replacement, sibling-temp writes, and cross-device move fallback helpers through `plugin-sdk/security-runtime`, and move OpenClaw's duplicated safe filesystem write paths onto the shared `@openclaw/fs-safe` package.
|
||||
- Plugin SDK/fs-safe: route browser, media, channel, and QA external output producers through staged fs-safe writes before final publication. (#78768)
|
||||
- Plugin SDK/fs-safe: rename the public temp workspace helpers to `tempWorkspace`, `withTempWorkspace`, `tempWorkspaceSync`, and `withTempWorkspaceSync`, matching the cleaner `@openclaw/fs-safe` API before the package is published.
|
||||
- Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
- Providers/OpenRouter: expand app-attribution categories so OpenClaw advertises coding, programming, writing, chat, and personal-agent usage on verified OpenRouter routes. Thanks @vincentkoc.
|
||||
- Agents/performance: pass the resolved workspace through BTW, compaction, embedded-run model generation, and PDF model setup so explicit agent-dir model refreshes can reuse the current workspace-scoped plugin metadata snapshot instead of falling back to cold plugin metadata scans. (#77519, #77532)
|
||||
- Plugins/performance: let unscoped model catalog and manifest-contract readers reuse the current workspace-compatible plugin metadata snapshot, avoiding repeated cold plugin metadata scans on hot control-plane paths while preserving env/config/workspace compatibility checks. (#77519, #77532)
|
||||
- Core/performance: trim reply payload routing, heartbeat filtering, tool display, core tool assembly, channel directory, task status, and Slack approval formatting helper chains with direct bounded scans. Thanks @vincentkoc.
|
||||
- Agents/sandbox: store sandbox container and browser registry entries as per-runtime shard files, reducing unrelated session lock contention while `openclaw doctor --fix` migrates legacy monolithic registry files. (#74831) Thanks @luckylhb90.
|
||||
- Plugins/runtime state: add `registerIfAbsent` for atomic keyed-store dedupe claims that return whether a plugin successfully claimed a key without overwriting an existing live value. Thanks @amknight.
|
||||
- Exec approvals: add a tree-sitter-backed shell command explainer for future approval and command-review surfaces. (#75004) Thanks @jesse-merhi.
|
||||
- Control UI/performance: record browser long animation frame or long task entries in the debug event log when supported, making slow dashboard renders easier to attribute from the UI.
|
||||
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and default sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
|
||||
- Control UI/performance: keep chat, config, and channel refreshes responsive by decoupling slow history/schema/status work, reducing the client history window, and logging over-budget chat/config renders. Refs #77060, #45698, #47979, #44107. Thanks @BunsDev.
|
||||
- Gateway/diagnostics: add startup phase spans, active work labels, stale terminal bridge markers, and opt-in sync-I/O tracing in `pnpm gateway:watch` so slow Gateway turns are easier to attribute from logs and stability diagnostics.
|
||||
- QA/Codex harness: add targeted live Docker/Testbox diagnostics, auth preflight checks, cache mount fixes, and app-server protocol checkout discovery so maintainer harness failures are easier to reproduce. Thanks @vincentkoc.
|
||||
- QA/Mantis: add `pnpm openclaw qa mantis slack-desktop-smoke` to run Slack live QA inside a Crabbox VNC desktop, open Slack Web, and capture desktop screenshots beside the Slack QA artifacts.
|
||||
- QA/Mantis: add visual desktop tasks with Crabbox MP4 recording, screenshot capture, and optional image-understanding assertions, and preserve video artifacts in Mantis before/after reports.
|
||||
@@ -117,6 +145,7 @@ Docs: https://docs.openclaw.ai
|
||||
- QA/Mantis: accept Blacksmith Testbox `tbx_...` lease ids from desktop smoke warmup, so provider overrides do not fail before inspect/run. Thanks @vincentkoc.
|
||||
- Plugins/SDK: add bounded `before_agent_finalize` retry instructions so workflow plugins can request one more model pass. Thanks @100yenadmin.
|
||||
- Plugin SDK: add plugin-owned `SessionEntry` slot projection and scoped trusted-policy session extension reads. (#75609; replaces part of #73384/#74483) Thanks @100yenadmin.
|
||||
- Control UI/WebChat: show a persistent compact context usage indicator from fresh session token data before the high-pressure warning state, while keeping the existing compaction prompt threshold. Fixes #46398; refs #45048, #50071, and #73744. Thanks @walterwkchoy, @AxelrodAI, @Brissux, @vincentkoc, and @BunsDev.
|
||||
- Docs: clarify that IRC uses raw TCP/TLS sockets outside operator-managed forward proxy routing, so direct IRC egress should be explicitly approved before enabling IRC. Thanks @jesse-merhi.
|
||||
- Dependencies: refresh runtime and provider packages including Pi 0.73.0, ACPX adapters, OpenAI, Anthropic, Slack, and TypeScript native preview, while keeping the Bedrock runtime installer override pinned below the Windows ARM Node 24 npm resolver failure.
|
||||
- Contributor PRs: require external pull requests to include after-fix real behavior proof from a real OpenClaw setup, with terminal screenshots, console output, redacted runtime logs, linked artifacts, and copied live output treated as valid evidence while unit tests, mocks, lint, typechecks, snapshots, and CI remain supplemental only.
|
||||
@@ -127,23 +156,84 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/hooks: add a `before_agent_run` pass/block gate that can stop a user prompt before model submission while preserving a redacted transcript entry for the user, and clarify that raw conversation hooks require `hooks.allowConversationAccess=true`. (#75035) Thanks @jesse-merhi.
|
||||
- Config/Nix: keep startup-derived plugin enablement, gateway auth tokens, control UI origins, and owner-display secrets runtime-only instead of rewriting `openclaw.json`; in Nix mode, config writers, mutating `openclaw update`, plugin lifecycle mutators, and doctor repair/token-generation now refuse with agent-first nix-openclaw guidance. (#78047) Thanks @joshp123.
|
||||
- Agents/context engine: invalidate cached assembled context views when source history shrinks or assembly fails, preventing stale pre-reset history from being reused. Fixes #77968. (#78163) Thanks @brokemac79 and @ChrisBot2026.
|
||||
- Plugin SDK: add a generic `api.runtime.llm.complete` host completion helper with runtime-derived caller attribution, config-gated model/agent overrides, session-bound context-engine access, request-scoped config, audit metadata, and normalized usage attribution. (#64294) Thanks @DaevMithran.
|
||||
|
||||
### Breaking
|
||||
|
||||
- Channels/iMessage: remove the bundled BlueBubbles channel surface and deprecate BlueBubbles-backed iMessage setup in OpenClaw. Existing `channels.bluebubbles` configs must migrate to `channels.imessage` using `imsg` on a signed-in Mac or an SSH wrapper, and non-macOS default `imsg` configs now report remote-Mac wrapper guidance.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Control UI/chat: hide retired and non-public Google Gemini model IDs from chat model catalogs and route the bare `gemini-3-pro` alias to Gemini 3.1 Pro Preview instead of the shut-down Gemini 3 Pro Preview. Thanks @BunsDev.
|
||||
- Gateway/watch: leave `OPENCLAW_TRACE_SYNC_IO` disabled by default in `pnpm gateway:watch:raw` so watch mode avoids noisy Node sync-I/O stack traces unless explicitly requested.
|
||||
- Codex app-server: close stdio stdin before force-killing the managed app-server, matching Codex single-client shutdown behavior and avoiding unsettled CLI exits after successful runs.
|
||||
- CLI/Codex: dispose registered agent harnesses during short-lived CLI shutdown so successful Codex-backed `agent --local` runs do not leave app-server child processes alive.
|
||||
- Agents/Codex: auto-enable the Codex harness plugin for one-shot OpenAI model overrides so `openclaw agent --local --model openai/...` does not fail with an unregistered `codex` harness.
|
||||
- Gateway/live tests: avoid full model-registry enumeration for explicit provider-qualified live model filters, preventing `.profile` OpenAI gateway profile runs from hanging before provider dispatch.
|
||||
- Gateway/status: surface CLI and gateway runtime versions, warn about stale PATH/global wrappers when they differ, and add stale-wrapper checks to the newer-config warning. Refs #79091. Thanks @RamaAditya49 and @sallyom.
|
||||
- Providers: preserve non-OK `text/event-stream` response bodies so provider HTTP errors keep their JSON detail instead of collapsing to generic streaming failures. Fixes #78180.
|
||||
- Gateway/auth: make explicit `trusted-proxy` mode fail closed instead of accepting local password fallback credentials after trusted-proxy identity checks fail. Fixes #78684.
|
||||
- Active memory: treat Google Chat `spaces/...` conversation ids as scoped targets instead of runnable channel names so recall runs no longer fail bundled-plugin dirName validation. Fixes #78918.
|
||||
- Active memory: make `/active-memory status` honor the configured agent allowlist instead of reporting on for agents where recall is disabled. Fixes #78986.
|
||||
- Mistral: normalize structured OpenAI-compatible completions content blocks so thinking objects are not persisted as `[object Object]` visible reply text. Fixes #78846.
|
||||
- Tools/session status: render the active heartbeat/run model for `session_status({"sessionKey":"current"})` instead of falling back to the persisted session default. Fixes #77493.
|
||||
- Doctor/secrets: allow safe inherited exec SecretRef `passEnv` names such as `HOME` while still blocking dangerous runtime env hooks. Fixes #78216.
|
||||
- Chat commands: make `/model default` reset the session model override instead of treating it as a literal model name. Fixes #78182.
|
||||
- Cron: make rejected `payload.model` errors show the configured `agents.defaults.models` allowlist instead of echoing the rejected model twice. Fixes #79058.
|
||||
- Agents/subagents: retry parent wake announces when the announce-summary model run fails with fallback cooldown exhaustion instead of dropping the wake on the first transient provider overload. Refs #78581.
|
||||
- Providers/network: honor IPv4 CIDR and octet-wildcard `NO_PROXY` entries such as `100.64.0.0/10` and `100.64.*` before enabling trusted env-proxy mode for model-provider requests. Fixes #79030.
|
||||
- Skills: cap skills watcher directory traversal at the same depth used by skill discovery so large non-skill trees under configured skill roots do not exhaust file descriptors on startup. Fixes #75501. Thanks @wzq-xzwj.
|
||||
- Docs/Docker: document a local Compose override for Docker Desktop DNS failures in the shared-network `openclaw-cli` sidecar, keeping the default compose setup hardened while unblocking `openclaw plugins install` when users opt in. Fixes #79018. Thanks @Jason-Vaughan.
|
||||
- Installer: when npm installs `openclaw` outside the parent shell PATH, print follow-up commands with the resolved binary path instead of telling users to run `openclaw` from a shell that will report `command not found`. Fixes #72382. Thanks @jbob762.
|
||||
- Plugins/runtime: share MIME and JSON Schema helpers across bundled plugins while preserving canonical media MIME inference, browser URL wildcard semantics, migration home-path resolution, QA request-limit responses, and extensionless text file previews.
|
||||
- Compute plugin callback authorization dynamically [AI]. (#78866) Thanks @pgondhi987.
|
||||
- fix(active-memory): require admin scope for global toggles [AI]. (#78863) Thanks @pgondhi987.
|
||||
- Honor owner enforcement for native commands [AI]. (#78864) Thanks @pgondhi987.
|
||||
- Tavily: resolve dedicated `tavily_search` and `tavily_extract` tool credentials from the active runtime config snapshot, so `exec` SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.
|
||||
- Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.
|
||||
- fix(auto-reply): gate inline skill tool dispatch [AI]. (#78517) Thanks @pgondhi987.
|
||||
- Canvas plugin: keep legacy root `canvasHost` configs valid until `openclaw doctor --fix` migrates them into `plugins.entries.canvas.config.host`, move Canvas/A2UI clients to gateway protocol v4 plugin surfaces, and refresh the generated A2UI bundle hash so normal builds stay clean.
|
||||
- feishu: honor config write policy for dynamic agents [AI]. (#78520) Thanks @pgondhi987.
|
||||
- fix(skill-workshop): honor pending approval for tool suggestions [AI]. (#78516) Thanks @pgondhi987.
|
||||
- BytePlus: mark Kimi K2.5 and Kimi K2 Thinking catalog entries as reasoning-capable, raise their output cap to 32k tokens, and fill Kimi cache-read pricing. Fixes #54149.
|
||||
- Control UI/chat: wait for an in-flight model dropdown patch before sending the next chat message, so immediate sends use the selected session model instead of racing the previous override. Fixes #54240.
|
||||
- Native chat: decode gateway-provided thinking metadata for the iOS/macOS picker so provider-specific levels such as `adaptive`, `xhigh`, and `max` appear without leaking unsupported default-model options. Thanks @BunsDev.
|
||||
- Agents/compaction: cap summarization output reserve tokens to the selected model's `maxTokens` so 1M-context Anthropic compactions do not request more output than the API permits. Fixes #54383.
|
||||
- Agents/tools: fail `exec host=node` before `system.run` when the selected node is known to be disconnected, with an actionable reconnect message instead of a raw node invoke failure. Thanks @BunsDev.
|
||||
- Agents/models: accept legacy `anthropic-cli/*` model refs as Claude CLI runtime refs instead of failing model resolution with `Unknown model`. Thanks @BunsDev.
|
||||
- Agents/tools: keep restrictive-profile tool-section warnings scoped to the configured sections whose tools are still missing from `alsoAllow`, so already re-allowed filesystem tools do not make exec-only fixes look broader than they are. Thanks @BunsDev.
|
||||
- Agents/tools: avoid warning messaging-only agents about inherited global `tools.exec` or `tools.fs` sections when the agent profile did not configure those tool sections itself. Thanks @BunsDev.
|
||||
- Codex dynamic tools: normalize runtime `toolsAllow` entries the same way as Pi tool policy, so aliases like `bash` and `apply-patch` still expose the intended OpenClaw tools. Thanks @BunsDev.
|
||||
- Memory/dreaming: read OpenAI-style `output_text` assistant parts from narrative subagent transcripts, so light-phase Dream Diary entries are not dropped as empty. Thanks @BunsDev.
|
||||
- OpenAI-compatible providers: honor `compat.supportsTools=false` by stripping tool payload fields before dispatch to chat-only endpoints. Fixes #74664.
|
||||
- OpenAI-compatible providers: apply model-declared unsupported tool-schema keyword stripping to native OpenAI transport payloads and mark Fireworks Kimi K2.5 as rejecting `not` schemas. Fixes #75467.
|
||||
- OpenAI-compatible gateway: sanitize images supplied through request content even when the prompt text contains no image file references, preventing oversized attachment payloads from bypassing the resize/drop pipeline. Fixes #59913.
|
||||
- Auth profiles: normalize inline API keys and tokens loaded from `auth-profiles.json` so masked or rich-text credential artifacts fail as auth errors instead of crashing HTTP header construction. Fixes #77624.
|
||||
- llm-task: resolve configured model aliases before embedded dispatch so `model="gemini-flash"` and other aliases route to the intended provider instead of the agent default. Fixes #54166.
|
||||
- Media generation: resolve slash-containing model-only overrides like `fal-ai/flux/dev` through registered provider model metadata so FAL image/video models do not get misparsed as provider `fal-ai`. Fixes #77444.
|
||||
- Commands/BTW: show the `/btw` missing-question usage placeholder with brackets so outbound channel sanitization keeps it visible. Fixes #62877. Thanks @RajvardhanPatil07.
|
||||
- CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541.
|
||||
- Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858.
|
||||
- Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745.
|
||||
- Amazon Bedrock: refresh shared AWS profile/config file credentials before Bedrock model, discovery, and embedding requests so long-running Gateway processes pick up renewed profile credentials without restart. Fixes #77551.
|
||||
- Amazon Bedrock: treat named `aws-sdk` auth profiles as config routing metadata instead of stored credentials, and let `doctor --fix` move legacy markers out of `auth-profiles.json`. Fixes #69708.
|
||||
- Anthropic: reject uppercase provider-prefixed forward-compat model ids locally instead of sending malformed dynamic ids upstream. Fixes #73715.
|
||||
- OpenAI/embeddings: pass configured output dimensionality through single and batched embedding requests so memory embedding indexes can request smaller vectors. Fixes #55126.
|
||||
- CLI/infer: normalize HEIC/HEIF image files to JPEG before model-run requests, avoiding providers that reject Apple image container formats. Fixes #50081.
|
||||
- CLI/infer: fall back to macOS `sips` when optional image tooling cannot decode HEIC/HEIF input files before model-run requests. Refs #50081.
|
||||
- OpenRouter: keep the default `openrouter/auto` model ref canonical while preventing TUI and Control UI catalog pickers from displaying or submitting `openrouter/openrouter/auto`. Fixes #62655.
|
||||
- Status/Claude CLI: show `oauth (claude-cli)` for working Claude CLI OAuth runtime sessions instead of `unknown` when no local auth profile exists. Fixes #78632. Thanks @gorkem2020.
|
||||
- Memory search: preserve keyword-only hybrid FTS matches when vector scoring is unavailable or below the configured minimum score, so exact lexical hits are not dropped by weighted min-score filtering.
|
||||
- Exec approvals/node: let trusted backend node invokes complete no-device Control UI approvals after the original request connection changes, while keeping node, command, cwd, env, and allow-once replay bindings enforced. Fixes #78569. Thanks @naturedogdog.
|
||||
- Agents/subagents: keep background completion delivery on the requester-agent handoff/queue-retry path instead of raw-sending child results directly, and strip child-result wrapper or OpenClaw runtime-context scaffolding from queued outbound retries. Fixes #78531. Thanks @EthanSK.
|
||||
- Sandbox: recreate cached browser bridges when JavaScript-evaluation permission changes, keep failed prune removals tracked for retry, and make cross-device directory moves copy-then-commit without partially emptying the source on failure.
|
||||
- CLI/completion: guard the shell-profile source line written by `openclaw completion --install` with a file existence check (`[ -f ... ] && source ...` for bash/zsh, `test -f ...; and source ...` for fish) so uninstalling OpenClaw no longer makes new login shells error on a missing completion cache. (#78659) Thanks @sjf.
|
||||
- Cron/doctor: repair persisted cron jobs whose `payload.model` was stored as `"default"`, `"null"`, blank, or JSON `null` by removing the bad override during `openclaw doctor --fix` while keeping cron runtime model validation strict. Fixes #78549. Thanks @bizzle12368239.
|
||||
- Telegram: honor `accessGroup:*` sender allowlists for DMs, groups, native commands, and callback authorization before applying Telegram's numeric sender-ID checks. Fixes #78660. Thanks @manugc.
|
||||
- Agent delivery: report `deliverySucceeded=false` when outbound delivery returns no adapter result, so claimed/empty delivery paths no longer masquerade as successful sends. Fixes #78532. Thanks @joeyfrasier.
|
||||
- Cron/isolated runs: fail implicit announce delivery before model execution when `delivery.channel=last` has no previous route, so recurring jobs do not spend tokens before hitting a permanent delivery-target error. Fixes #78608. Thanks @sallyom.
|
||||
- Gateway/sessions: persist a new generated transcript file when daily gateway-agent session rollover changes the session id, while preserving custom transcript paths. Fixes #78607. Thanks @nailujac, @zerone0x, and @sallyom.
|
||||
- Doctor/OpenAI Codex: revert the 2026.5.5 `doctor --fix` repair that rewrote valid `openai-codex/*` ChatGPT/Codex OAuth routes to `openai/*`, which could break OAuth-only GPT-5.5 setups or accidentally move users onto the OpenAI API-key route. If 2026.5.5 already changed your default model, run `openclaw models set openai-codex/gpt-5.5 && openclaw config validate` to switch the default agent back to the Codex OAuth PI route. Fixes #78407.
|
||||
- Doctor/OpenAI Codex: repair legacy `openai-codex/*` agent model refs and stale OpenAI PI session pins to `openai/*` with the Codex runtime, preserving existing `openai-codex` auth profiles so ChatGPT/Codex OAuth users do not fall back to OpenAI API-key routing. Fixes #78407.
|
||||
- Telegram: keep the polling watchdog tied to `getUpdates` liveness so unrelated outbound Bot API calls cannot mask a wedged inbound poller. Fixes #78422. Thanks @ai-hpc.
|
||||
- Discord/groups: instruct group-chat agents to stay silent when a message is addressed to someone else, replying only when invited or correcting key facts. (#78615)
|
||||
- Discord/groups: tell Discord-channel agents to wrap bare URLs as `<https://example.com>` so link previews do not expand into uninvited embeds. (#78614)
|
||||
@@ -211,6 +301,7 @@ Docs: https://docs.openclaw.ai
|
||||
- WebChat/Codex media: stage Codex app-server generated local images into managed media before Gateway display, so Codex-home image paths no longer hit `LocalMediaAccessError` while keeping Codex home out of the display allowlist. Thanks @frankekn.
|
||||
- Plugins/update: repair plugin-local `openclaw` peer links for all recorded npm plugins after any npm update mutates the shared managed npm tree, so targeted or batch updates cannot leave Codex, Discord, or Brave with pruned SDK imports. (#77787) Thanks @ProspectOre.
|
||||
- Codex harness: honor `models.providers.openai-codex.models[].contextTokens` for native `openai/*` Codex runtime runs and `/status` context reporting, so subscription-backed Codex agents use the configured OAuth context cap without inflating past the runtime model window. Fixes #77858. Thanks @lilesjtu.
|
||||
- Sessions cleanup: add `openclaw sessions cleanup --fix-dm-scope` so operators who return `session.dmScope` to `main` can dry-run and retire stale direct-DM session rows while preserving transcripts as deleted archives. Fixes #47561 and #45554. Thanks @BunsDev.
|
||||
- TUI/sessions: bound the session picker to recent rows and use exact lookup-style refreshes for the active session, so dusty stores no longer make TUI hydrate weeks-old transcripts before becoming responsive. Thanks @vincentkoc.
|
||||
- Doctor/gateway: report recent supervisor restart handoffs in `openclaw doctor --deep`, using the installed service environment when available so service-managed clean exits are visible in guided diagnostics. Thanks @shakkernerd.
|
||||
- Gateway/status: show recent supervisor restart handoffs in `openclaw gateway status --deep`, including JSON details, so clean service-managed restarts are reported as restart handoffs instead of opaque stopped-service diagnostics. Thanks @shakkernerd.
|
||||
@@ -236,6 +327,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/update: make dev-channel preflight lint opt-in and constrained when enabled, so `openclaw update --channel dev` no longer walks back otherwise-good main commits when Ubuntu hosts OOM-kill or fail parallel oxlint shards. Thanks @vincentkoc.
|
||||
- Google Meet: fork the caller's current agent transcript into agent-mode meeting consultant sessions, so Meet replies inherit the context from the tool call that joined the meeting.
|
||||
- Google Meet: log the concrete agent-mode TTS provider, model, voice, output format, and sample rate after speech synthesis, so Meet logs show which voice backend spoke each reply.
|
||||
- Control UI/Sessions: hide disk-discovered unregistered-agent sessions by default and fall back from restored unconfigured agent session keys before chat refresh, preventing deleted-agent stores from reopening the wrong workspace. Fixes #41685. Thanks @BunsDev.
|
||||
- Google Meet: log the resolved audio provider model when starting Chrome and paired-node Meet talk-back bridges, so agent-mode joins show the STT model and bidi joins show the realtime voice model.
|
||||
- Google Meet: stop advertising legacy `mode: "realtime"` to agents and config UIs, while keeping it as a hidden compatibility alias for `mode: "agent"`, so new joins use the STT -> OpenClaw agent -> TTS path instead of selecting the direct realtime voice fallback.
|
||||
- Google Meet: add `chrome.audioBufferBytes` for generated command-pair SoX audio commands and lower the default buffer from SoX's 8192 bytes to 4096 bytes to reduce Chrome talk-back latency.
|
||||
@@ -496,6 +588,10 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/subagents: have completed session-mode subagent registry rows honor `agents.defaults.subagents.archiveAfterMinutes` (default 60 minutes; same knob run-mode already uses for `archiveAtMs`) instead of a hardcoded 5-minute TTL, so `subagents list` and other registry-backed surfaces still show recently-completed runs and operators have one consistent retention knob across spawn modes. (#78263) Thanks @arniesaha.
|
||||
- Plugins/channel setup: fix `setChannelRuntime` being silently dropped from non-bundled external plugin setup entries — external channel plugins that export `{ plugin, setChannelRuntime }` from their setup entry now have the runtime setter invoked, so the runtime initializer the provider polls for is set before the channel starts, preventing a poll timeout and gateway crash loop when the plugin opts into deferred startup loading. Fixes #77779. (#77799) Thanks @openperf.
|
||||
- WhatsApp: route proactive phone-number sends through Baileys LID forward mappings when available, so LID-addressed contacts receive agent messages instead of creating sender-only ghost chats. Fixes #67378. (#74925) Thanks @edenfunf.
|
||||
- WhatsApp: send captioned `MEDIA:` directive auto-replies once instead of emitting an empty media message before the captioned media reply. (#78770) Thanks @ai-hpc.
|
||||
- Hooks/cron: log returned `/hooks/agent` isolated-run errors and failed cron jobs with cron diagnostic summaries, so rejected `payload.model` values are visible instead of looking like accepted-but-missing runs. Fixes #78597. (#78655) Thanks @kevinslin.
|
||||
- Managed proxy/security: classify raw socket callsites and proxy runtime mutations in boundary checks so new direct egress or unmanaged proxy-state changes cannot land without explicit review. (#77126) Thanks @jesse-merhi.
|
||||
- Channels/iMessage: surface the silent group-allowlist drop at default log level by emitting a one-time `warn` per account at monitor startup when `channels.imessage.groupPolicy: "allowlist"` is set without a `channels.imessage.groups` block, plus a one-time `warn` per `chat_id` when the runtime gate drops a specific group, naming the exact `channels.imessage.groups[...]` key to add to allow it. Fixes #78749. (#79190) Thanks @omarshahine.
|
||||
|
||||
## 2026.5.3-1
|
||||
|
||||
@@ -521,6 +617,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Tools/BTW: add `/side` as a text and native slash-command alias for `/btw` side questions.
|
||||
- Doctor/config: `doctor --fix` now commits safe legacy migrations even when unrelated validation issues (e.g. a missing plugin) prevent full validation from passing, so `agents.defaults.llm` and other known-legacy keys are always cleaned up by `doctor --fix` regardless of other config problems. Fixes #76798. (#76800) Thanks @hclsys.
|
||||
- Agents/tools: skip optional media and PDF tool factories when the effective tool denylist already blocks them, avoiding unnecessary hot-path setup for tools that will be filtered out before model use. (#76773) Thanks @dorukardahan.
|
||||
- Agents/compaction: ignore pre-usage transcript metadata bytes when stale token snapshots estimate preflight compaction pressure, while still counting post-usage transcript tail pressure. Fixes #78604. Thanks @amknight.
|
||||
- Discord/status: let explicit reaction tool calls opt into tracking subsequent tool progress on the reacted message with `trackToolCalls: true`, and use the shared tool display emoji table for status reactions.
|
||||
- Gateway/config: stop Gateway startup and hot reload from auto-restoring invalid config; invalid config now fails closed and `openclaw doctor --fix` owns last-known-good repair.
|
||||
- Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup.
|
||||
|
||||
@@ -29,7 +29,7 @@ Welcome to the lobster tank! 🦞
|
||||
- **Ayaan Zaidi** - Telegram subsystem, Android app
|
||||
- GitHub: [@obviyus](https://github.com/obviyus) · X: [@obviyus](https://x.com/obviyus)
|
||||
|
||||
- **Tyler Yust** - Agents/subagents, cron, BlueBubbles, macOS app
|
||||
- **Tyler Yust** - Agents/subagents, cron, iMessage, macOS app
|
||||
- GitHub: [@tyler6204](https://github.com/tyler6204) · X: [@tyleryust](https://x.com/tyleryust)
|
||||
|
||||
- **Mariano Belinky** - iOS app, Security
|
||||
@@ -103,7 +103,7 @@ For coordinated change sets that genuinely need more than 20 PRs, join the **#cl
|
||||
## Before You PR
|
||||
|
||||
- Test locally with your OpenClaw instance
|
||||
- External PRs must include a filled **Real behavior proof** section in the PR body. Show the real setup you tested, the exact command or steps you ran after the patch, after-fix evidence, the observed result, and anything you did not test. Screenshots, recordings, terminal screenshots, console output, copied live output, linked artifacts, and redacted runtime logs all count. Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence. Unit tests, mocks, snapshots, lint, typechecks, and CI are useful but do not satisfy this requirement by themselves. Maintainers may apply `proof: override` only when the proof gate should not apply.
|
||||
- External PRs must include a filled **Real behavior proof** section in the PR body. Show the real setup you tested, the exact command or steps you ran after the patch, after-fix evidence, the observed result, and anything you did not test. Screenshots, recordings, terminal screenshots, console output, copied live output, linked artifacts, and redacted runtime logs all count. Unit tests, mocks, snapshots, lint, typechecks, and CI are useful but do not satisfy this requirement by themselves. Maintainers may apply `proof: override` only when the proof gate should not apply.
|
||||
- Run tests: `pnpm build && pnpm check && pnpm test`
|
||||
- For iterative local commits, `scripts/committer --fast "message" <files...>` passes `FAST_COMMIT=1` through to the pre-commit hook so it skips the repo-wide `pnpm check`. Only use it when you've already run equivalent targeted validation for the touched surface.
|
||||
- For extension/plugin changes, run the fast local lane first:
|
||||
@@ -164,7 +164,7 @@ Built with Codex, Claude, or other AI tools? **Awesome - just mark it!**
|
||||
Please include in your PR:
|
||||
|
||||
- [ ] Mark as AI-assisted in the PR title or description
|
||||
- [ ] Include human-run real behavior proof from your own setup. Redact private information like IP addresses, API keys, phone numbers, or non-public endpoints before posting evidence. AI-generated tests, mocks, lint, typechecks, and CI output are supplemental only; they do not prove the fix works for users.
|
||||
- [ ] Include human-run real behavior proof from your own setup. AI-generated tests, mocks, lint, typechecks, and CI output are supplemental only; they do not prove the fix works for users.
|
||||
- [ ] Include prompts or session logs if possible (super helpful!)
|
||||
- [ ] Confirm you understand what the code does
|
||||
- [ ] If you have access to Codex, run `codex review --base origin/main` locally and address the findings before asking for review
|
||||
|
||||
@@ -97,9 +97,9 @@ RUN for dir in /app/${OPENCLAW_BUNDLED_PLUGIN_DIR} /app/.agent /app/.agents; do
|
||||
# Stub it so local cross-arch builds still succeed.
|
||||
RUN pnpm canvas:a2ui:bundle || \
|
||||
(echo "A2UI bundle: creating stub (non-fatal)" && \
|
||||
mkdir -p src/canvas-host/a2ui && \
|
||||
echo "/* A2UI bundle unavailable in this build */" > src/canvas-host/a2ui/a2ui.bundle.js && \
|
||||
echo "stub" > src/canvas-host/a2ui/.bundle.hash && \
|
||||
mkdir -p extensions/canvas/src/host/a2ui && \
|
||||
echo "/* A2UI bundle unavailable in this build */" > extensions/canvas/src/host/a2ui/a2ui.bundle.js && \
|
||||
echo "stub" > extensions/canvas/src/host/a2ui/.bundle.hash && \
|
||||
rm -rf vendor/a2ui apps/shared/OpenClawKit/Tools/CanvasA2UI)
|
||||
RUN pnpm build:docker
|
||||
# Force pnpm for UI build (Bun may fail on ARM/Synology architectures)
|
||||
|
||||
17
README.md
17
README.md
@@ -23,7 +23,7 @@ It answers you on the channels you already use. It can speak and listen on macOS
|
||||
|
||||
If you want a personal, single-user assistant that feels local, fast, and always-on, this is it.
|
||||
|
||||
Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, BlueBubbles, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat.
|
||||
Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat.
|
||||
|
||||
[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd)
|
||||
|
||||
@@ -96,7 +96,7 @@ Model note: while many providers and models are supported, prefer a current flag
|
||||
|
||||
## Install (recommended)
|
||||
|
||||
Runtime: **Node 24 (recommended) or Node 22.14+**.
|
||||
Runtime: **Node 24 (recommended) or Node 22.16+**.
|
||||
|
||||
```bash
|
||||
npm install -g openclaw@latest
|
||||
@@ -109,7 +109,7 @@ OpenClaw Onboard installs the Gateway daemon (launchd/systemd user service) so i
|
||||
|
||||
## Quick start (TL;DR)
|
||||
|
||||
Runtime: **Node 24 (recommended) or Node 22.14+**.
|
||||
Runtime: **Node 24 (recommended) or Node 22.16+**.
|
||||
|
||||
Full beginner guide (auth, pairing, channels): [Getting started](https://docs.openclaw.ai/start/getting-started)
|
||||
|
||||
@@ -121,7 +121,7 @@ openclaw gateway --port 18789 --verbose
|
||||
# Send a message
|
||||
openclaw message send --target +1234567890 --message "Hello from OpenClaw"
|
||||
|
||||
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/BlueBubbles/IRC/Microsoft Teams/Matrix/Feishu/LINE/Mattermost/Nextcloud Talk/Nostr/Synology Chat/Tlon/Twitch/Zalo/Zalo Personal/WeChat/QQ/WebChat)
|
||||
# Talk to the assistant (optionally deliver back to any connected channel: WhatsApp/Telegram/Slack/Discord/Google Chat/Signal/iMessage/IRC/Microsoft Teams/Matrix/Feishu/LINE/Mattermost/Nextcloud Talk/Nostr/Synology Chat/Tlon/Twitch/Zalo/Zalo Personal/WeChat/QQ/WebChat)
|
||||
openclaw agent --message "Ship checklist" --thinking high
|
||||
```
|
||||
|
||||
@@ -146,7 +146,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
|
||||
## Highlights
|
||||
|
||||
- **[Local-first Gateway](https://docs.openclaw.ai/gateway)** — single control plane for sessions, channels, tools, and events.
|
||||
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, BlueBubbles (iMessage), iMessage (legacy), IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat, macOS, iOS/Android.
|
||||
- **[Multi-channel inbox](https://docs.openclaw.ai/channels)** — WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat, macOS, iOS/Android.
|
||||
- **[Multi-agent routing](https://docs.openclaw.ai/gateway/configuration)** — route inbound channels/accounts/peers to isolated agents (workspaces + per-agent sessions).
|
||||
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS and continuous voice on Android (ElevenLabs + system TTS fallback).
|
||||
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
|
||||
@@ -246,18 +246,13 @@ Note: `pnpm openclaw ...` runs TypeScript directly (via `tsx`). `pnpm build` pro
|
||||
|
||||
## Development channels
|
||||
|
||||
- **stable**: tagged releases (`vYYYY.M.D` today), npm dist-tag `latest`.
|
||||
- **stable**: tagged releases (`vYYYY.M.D` or `vYYYY.M.D-<patch>`), npm dist-tag `latest`.
|
||||
- **beta**: prerelease tags (`vYYYY.M.D-beta.N`), npm dist-tag `beta` (macOS app may be missing).
|
||||
- **dev**: moving head of `main`, npm dist-tag `dev` (when published).
|
||||
|
||||
Switch channels (git + npm): `openclaw update --channel stable|beta|dev`.
|
||||
Details: [Development channels](https://docs.openclaw.ai/install/development-channels).
|
||||
|
||||
We are planning SemVer-compatible monthly support lines using `YYYY.M.PATCH`
|
||||
versions, but they are not available yet. Legacy `vYYYY.M.D-<patch>` correction
|
||||
tags may still be recognized for older releases; new release work should not use
|
||||
that format as the long-term support model.
|
||||
|
||||
## Agent workspace + skills
|
||||
|
||||
- Workspace root: `~/.openclaw/workspace` (configurable via `agents.defaults.workspace`).
|
||||
|
||||
@@ -312,7 +312,7 @@ OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for *
|
||||
|
||||
### Node.js Version
|
||||
|
||||
OpenClaw requires **Node.js 22.14.0 or later** (LTS). This version includes important security patches:
|
||||
OpenClaw requires **Node.js 22.16.0 or later** (LTS). This version includes important security patches:
|
||||
|
||||
- CVE-2025-59466: async_hooks DoS vulnerability
|
||||
- CVE-2026-21636: Permission model bypass vulnerability
|
||||
@@ -320,7 +320,7 @@ OpenClaw requires **Node.js 22.14.0 or later** (LTS). This version includes impo
|
||||
Verify your Node.js version:
|
||||
|
||||
```bash
|
||||
node --version # Should be v22.14.0 or later
|
||||
node --version # Should be v22.16.0 or later
|
||||
```
|
||||
|
||||
### Docker Security
|
||||
|
||||
339
appcast.xml
339
appcast.xml
@@ -2,6 +2,53 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>OpenClaw</title>
|
||||
<item>
|
||||
<title>2026.5.7</title>
|
||||
<pubDate>Thu, 07 May 2026 22:36:27 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026050790</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.5.7</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.5.7</h2>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Release/plugin publishing: retry transient ClawHub CLI dependency install failures, keep preview-passing plugins publishable when one preview cell flakes, and verify every expected ClawHub package version after publish so maintenance releases are faster to recover and less likely to hide partial plugin publishes.</li>
|
||||
<li>OpenAI: support <code>openai/chat-latest</code> as an explicit direct API-key model override for trying the moving ChatGPT Instant API alias without changing the stable default model.</li>
|
||||
<li>Cron CLI: include computed <code>status</code> in <code>cron list --json</code> and <code>cron show --json</code> output so external tooling can read disabled/running/ok/error/skipped/idle state without reimplementing cron status derivation. (#78701) Thanks @aweiker.</li>
|
||||
<li>Channels CLI: make <code>openclaw channels list</code> channel-only, add <code>--all</code> for bundled and catalog channels, render installed/configured/enabled state, and move model auth/usage details to <code>openclaw models auth list</code>, <code>openclaw status</code>, and <code>openclaw models list</code>. (#78456) Thanks @sliverp.</li>
|
||||
<li>Native commands: honor owner enforcement for native command handlers. (#78864) Thanks @pgondhi987.</li>
|
||||
<li>Active Memory: require admin scope for global memory toggles. (#78863) Thanks @pgondhi987.</li>
|
||||
<li>Gateway/sessions: clear cached skills snapshots during <code>/new</code> and <code>sessions.reset</code> so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.</li>
|
||||
<li>Auto-reply: gate inline skill tool dispatch through before-tool-call authorization hooks. (#78517) Thanks @pgondhi987.</li>
|
||||
<li>Tavily: resolve dedicated <code>tavily_search</code> and <code>tavily_extract</code> tool credentials from the active runtime config snapshot, so <code>exec</code> SecretRef-backed API keys do not reach the tools unresolved. (#78610) Thanks @VACInc.</li>
|
||||
<li>Plugins/install: use the same absolute POSIX npm lifecycle shell for managed plugin install, rollback, repair, and uninstall npm operations as staged package updates, preventing restricted PATH shells from breaking cleanup. Thanks @vincentkoc.</li>
|
||||
<li>Agents/context engine: invalidate cached assembled context views when source history shrinks or assembly fails, preventing stale pre-reset history from being reused. Fixes #77968. (#78163) Thanks @brokemac79 and @ChrisBot2026.</li>
|
||||
<li>Discord/message: parse provider-prefixed targets like <code>discord:channel:<id></code> as channel sends instead of legacy Discord DM targets, so cross-channel agent <code>message(action="send")</code> calls no longer misroute channel IDs into misleading <code>Unknown Channel</code> failures. Fixes #78572.</li>
|
||||
<li>Agents/compaction: clamp compaction summary reserve tokens to each model's output limit so high-context compaction no longer requests invalid <code>max_tokens</code> values. (#54392) Thanks @adzendo.</li>
|
||||
<li>Commands/BTW: show the <code>/btw</code> missing-question usage placeholder with brackets so outbound channel sanitization keeps it visible. Fixes #62877. Thanks @RajvardhanPatil07.</li>
|
||||
<li>Cron/doctor: repair persisted cron jobs whose <code>payload.model</code> was stored as <code>"default"</code>, <code>"null"</code>, blank, or JSON <code>null</code> by removing the bad override during <code>openclaw doctor --fix</code> while keeping cron runtime model validation strict. Fixes #78549. Thanks @bizzle12368239.</li>
|
||||
<li>Telegram: honor <code>accessGroup:*</code> sender allowlists for DMs, groups, native commands, and callback authorization before applying Telegram's numeric sender-ID checks. Fixes #78660. Thanks @manugc.</li>
|
||||
<li>Agent delivery: report <code>deliverySucceeded=false</code> when outbound delivery returns no adapter result, so claimed/empty delivery paths no longer masquerade as successful sends. Fixes #78532. Thanks @joeyfrasier.</li>
|
||||
<li>Cron/isolated runs: fail implicit announce delivery before model execution when <code>delivery.channel=last</code> has no previous route, so recurring jobs do not spend tokens before hitting a permanent delivery-target error. Fixes #78608. Thanks @sallyom.</li>
|
||||
<li>Gateway/sessions: persist a new generated transcript file when daily gateway-agent session rollover changes the session id, while preserving custom transcript paths. Fixes #78607. Thanks @nailujac, @zerone0x, and @sallyom.</li>
|
||||
<li>Doctor/Codex OAuth: preserve working <code>openai-codex/*</code> PI routes during <code>doctor --fix</code> and recover 2026.5.5-rewritten <code>openai/*</code> GPT-5 routes when only Codex OAuth auth is available, so update repair does not break subscription-auth setups. Fixes #78407. Thanks @shakkernerd.</li>
|
||||
<li>Telegram: keep the polling watchdog tied to <code>getUpdates</code> liveness so unrelated outbound Bot API calls cannot mask a wedged inbound poller. Fixes #78422. Thanks @ai-hpc.</li>
|
||||
<li>Agents/subagents: have completed session-mode subagent registry rows honor <code>agents.defaults.subagents.archiveAfterMinutes</code> instead of a hardcoded 5-minute TTL, so registry-backed surfaces keep one retention knob across spawn modes. (#78263) Thanks @arniesaha.</li>
|
||||
<li>Plugins/channel setup: forward <code>setChannelRuntime</code> from non-bundled external plugin setup entries so deferred external channel runtime initializers are installed before startup polling. Fixes #77779. (#77799) Thanks @openperf.</li>
|
||||
<li>Telegram: treat successful same-chat <code>message</code> tool outbound sends during an inbound Telegram turn as delivered when deciding whether to emit the rewritten silent reply fallback. (#78685) Thanks @neeravmakwana.</li>
|
||||
<li>Gateway/tasks: reconcile stale CLI run-context tasks whose live run context disappeared and bound channel hot-reload deferrals so stale task records cannot block Discord/Slack/Telegram reloads forever.</li>
|
||||
<li>Discord/voice: audit Discord voice-channel permissions in <code>channels capabilities</code> and <code>channels status --probe</code>, including auto-join targets, so missing Connect/Speak/Read Message History permissions show up before <code>/vc join</code>.</li>
|
||||
<li>Discord/voice: make voice capture less choppy by extending the default post-speech silence grace to 2.5s, add <code>voice.captureSilenceGraceMs</code> for noisy Discord sessions, and tighten the spoken-output prompt around live STT fragments. Thanks @vincentkoc.</li>
|
||||
<li>WhatsApp: route proactive phone-number sends through Baileys LID forward mappings when available, so LID-addressed contacts receive agent messages instead of creating sender-only ghost chats. Fixes #67378. (#74925) Thanks @edenfunf.</li>
|
||||
<li>WhatsApp: send captioned <code>MEDIA:</code> directive auto-replies once instead of emitting an empty media message before the captioned media reply. (#78770) Thanks @ai-hpc.</li>
|
||||
<li>Codex/approvals: in Codex approval modes, stop installing the pre-guardian native <code>PermissionRequest</code> hook by default so Codex's reviewer can approve safe commands before OpenClaw surfaces an approval, remember <code>allow-always</code> decisions for identical Codex native <code>PermissionRequest</code> payloads within the active session window, and make plugin approval requests validate/render their actual allowed decisions so Telegram and other native approval UIs cannot offer stale actions. Thanks @shakkernerd.</li>
|
||||
<li>Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with fallback signatures, accept legacy <code>__env__:VAR</code> custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858.</li>
|
||||
<li>Telegram/models: parse provider ids containing dots in <code>/models</code> callback buttons so <code>hf.co</code> model lists render as inline keyboard buttons. Fixes #38745.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.7/OpenClaw-2026.5.7.zip" length="51130645" type="application/octet-stream" sparkle:edSignature="Zu+EzBGMRE1k7N4//L8HUxtUCPdO0ImrfDbgr2GrPMBrj7VGI1tOOl74gxNJoi/wfWvXz3fYVcBz2W/84ojuCw=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.5.2</title>
|
||||
<pubDate>Sun, 03 May 2026 01:11:51 +0000</pubDate>
|
||||
@@ -765,297 +812,5 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.29/OpenClaw-2026.4.29.zip" length="50896802" type="application/octet-stream" sparkle:edSignature="YfQ25zMGgDv8XvHbdlL/s0SMJXyu763l5ppnfjiKOjSyxZY9sfoLaoXthcctFQDXA8isR1EEb/EEausu+XkFCA=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.27</title>
|
||||
<pubDate>Wed, 29 Apr 2026 23:53:26 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026042790</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.27</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.27</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Sandbox/Docker: add opt-in <code>sandbox.docker.gpus</code> passthrough for Docker sandbox containers so local GPU workloads can run inside sandboxed agents when the host Docker runtime supports <code>--gpus</code>. Fixes #57976; carries forward #58124. Thanks @cyan-ember.</li>
|
||||
<li>iOS/Gateway: add an authenticated <code>node.presence.alive</code> protocol event and <code>node.list</code> last-seen fields so background iOS wakes can mark paired nodes recently alive without treating them as connected. Carries forward #63123. Thanks @ngutman.</li>
|
||||
<li>Android: publish authenticated <code>node.presence.alive</code> events after node connect and background transitions so paired Android nodes retain durable last-seen metadata after disconnects. Carries forward #63123. Thanks @ngutman.</li>
|
||||
<li>Gateway/chat: accept non-image attachments through <code>chat.send</code> by staging them as agent-readable media paths, while keeping unsupported RPC attachment paths explicit instead of silently dropping files. Fixes #48123. (#67572) Thanks @samzong.</li>
|
||||
<li>Security/networking: add opt-in operator-managed outbound proxy routing (proxy.enabled + proxy.proxyUrl/OPENCLAW_PROXY_URL) with strict http:// forward-proxy validation, loopback-only Gateway bypass, and cleanup of proxy env/dispatcher state on exit. (#70044) Thanks @jesse-merhi and @joshavant.</li>
|
||||
<li>Dependencies: refresh provider and tooling dependencies, including AWS SDK, PI runtime packages, AJV, Feishu SDK, Anthropic SDK, tokenjuice, and native TypeScript/oxlint tooling. Thanks @dependabot.</li>
|
||||
<li>Matrix/QA: add live Matrix approval scenarios for exec metadata, chunked fallback, plugin approvals, deny reactions, thread targeting, and <code>target: "both"</code> delivery, with redacted artifacts preserving safe approval summaries. Thanks @gumadeiras.</li>
|
||||
<li>Codex: add Computer Use setup for Codex-mode agents, including <code>/codex computer-use status/install</code>, marketplace discovery, optional auto-install, and fail-closed MCP server checks before Codex-mode turns start. Fixes #72094. (#71842) Thanks @pash-openai.</li>
|
||||
<li>Apps: consume Peekaboo 3.0.0-beta4 and ElevenLabsKit 0.1.1, align Swabble on Commander 0.2.2, and refresh macOS/iOS SwiftPM resolutions against the released dependency graph. Thanks @Blaizzy.</li>
|
||||
<li>Plugin SDK: expose shared channel route normalization, parser-driven target resolution, raw-target compact keys, parsed-target types, and route comparison helpers through <code>openclaw/plugin-sdk/channel-route</code>, switch native approval origin matching onto that route contract with optional delivery and match-only target normalization, and retire the internal channel-route shim behind dated compatibility aliases for legacy key/comparable-target helpers. Thanks @vincentkoc.</li>
|
||||
<li>Docs/Codex: document how Codex Computer Use, direct <code>cua-driver mcp</code>, and OpenClaw.app's PeekabooBridge fit together so desktop-control setup choices are clearer. Thanks @pash-openai and @trycua.</li>
|
||||
<li>Matrix/streaming: stream tool-progress updates into live Matrix preview edits by default when preview streaming is active, with <code>streaming.preview.toolProgress: false</code> to keep answer previews while hiding interim tool lines. Thanks @gumadeiras.</li>
|
||||
<li>Plugins/models: wire manifest <code>modelCatalog.aliases</code> and <code>modelCatalog.suppressions</code> into model-catalog planning and built-in model suppression, with stale Spark and Qwen Coding Plan suppressions now declared in plugin manifests instead of runtime fallback hooks. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/models: add a shared manifest-backed provider catalog builder and move Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Moonshot, DeepSeek, Tencent TokenHub, and StepFun provider catalogs onto their plugin manifest <code>modelCatalog</code> rows. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/models: move BytePlus and Volcano Engine standard and plan-provider catalogs into plugin manifest <code>modelCatalog</code> rows and remove the now-unused Volcengine-family shared catalog SDK subpath. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: move Fireworks and Together AI fixed provider catalogs into plugin manifest <code>modelCatalog</code> rows so provider-filtered listing can use manifest-backed static rows. Thanks @shakkernerd.</li>
|
||||
<li>Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (<code>openclaw-plugin-yuanbao</code>) in the official channel catalog, contract suites, and community plugin docs, with a new <code>docs/channels/yuanbao.md</code> quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.</li>
|
||||
<li>Channels/Yuanbao: add a channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.</li>
|
||||
<li>Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C <code>stream_messages</code> streaming with a <code>StreamingController</code> lifecycle manager, unified <code>sendMedia</code> with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via <code>createEngineAdapters()</code>. (#70624) Thanks @cxyhhhhh.</li>
|
||||
<li>Plugins/startup: migrate bundled plugin manifests to explicit <code>activation.onStartup</code> declarations so Gateway startup imports only the bundled plugins that intentionally register startup-time runtime surfaces. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add an opt-in future-mode gate for disabling deprecated implicit startup sidecar loading while preserving explicit startup and narrower activation triggers. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add plugin compatibility warnings for deprecated implicit startup loading so authors can migrate to explicit <code>activation.onStartup</code> metadata. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/runtime: load bundled agent tool-result middleware from manifest contracts on demand so tokenjuice stays startup-lazy without losing Pi/Codex tool-output compaction. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add explicit <code>activation.onStartup</code> metadata so plugins can declare Gateway startup import behavior while the deprecated implicit sidecar fallback remains for legacy plugins. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/startup: reuse lookup-table plugin manifests when loading startup plugins so Gateway boot avoids rebuilding plugin discovery and manifest metadata. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: declare fixed Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Chutes, Kilo, OpenAI, and OpenCode Go model catalogs in refreshable plugin manifests, keep broad <code>models list --all</code> on raw registry and supplement rows without runtime normalization, and avoid duplicate supplement resolution. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/runtime: reuse the current plugin metadata snapshot for provider discovery so repeated model-provider discovery avoids rebuilding plugin manifest metadata. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/startup: pass the plugin metadata snapshot from config validation into plugin bootstrap so startup reuses one manifest product instead of rebuilding plugin metadata. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/testing: move core-only channel contract fixtures under the channel contract test tree and retire the old <code>test/helpers/channels</code> bridge directory so plugin tests stay on focused SDK surfaces. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose native agent-runtime contract fixtures through <code>plugin-sdk/agent-runtime-test-contracts</code>, move sandbox config fixtures into the focused generic fixture subpath, and block extension tests from importing repo-only <code>test/helpers</code> bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose generic module reload, bundled-path, Node builtin mock, channel pairing/envelope, HTTP server, temp-home, replay-policy, and live STT helpers through focused SDK test subpaths so extension tests no longer depend on repo-only helper bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK: move maintained bundled channels off the deprecated <code>channel-config-schema-legacy</code> subpath, add an explicit bundled-channel schema SDK surface, and track both remaining legacy test/config compatibility barrels with dated removal windows. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose media provider capability assertions and provider HTTP mocks through focused SDK test subpaths, and retire the repo-only media-generation test helper bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: promote bundled plugin/provider/channel contract helpers to focused SDK test subpaths and retire the repo-only <code>test/helpers/plugins</code> TypeScript bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose generic channel action, setup, status, and directory contract helpers through <code>plugin-sdk/channel-test-helpers</code> so bundled extension tests no longer import repo-only channel helper bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add <code>plugin-sdk/channel-target-testing</code> for shared channel target-resolution cases, document channel reaction helpers on <code>plugin-sdk/channel-feedback</code>, and keep the old <code>plugin-sdk/test-utils</code> alias as compatibility-only. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add a focused generic fixture subpath for CLI capture, sandbox, skill, agent-message, system-event, terminal, chunking, auth-token, and typed-case helpers. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add focused plugin runtime and environment fixture subpaths so plugin tests can avoid the broad <code>plugin-sdk/testing</code> barrel for common setup helpers. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add a focused <code>plugin-sdk/plugin-test-api</code> helper subpath and move bundled plugin registration tests off the repo-only plugin API bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK: add generic host hooks for session state, next-turn context, trusted tool policy, UI descriptors, events, scheduler cleanup, and run-scoped plugin context. (#72287) Thanks @100yenadmin.</li>
|
||||
<li>Plugin SDK/testing: expose provider catalog, wizard, registry, manifest, public-artifact, outbound, and TTS contract helpers through documented SDK testing seams so bundled plugin tests no longer import repo <code>src/**</code> internals. Thanks @vincentkoc.</li>
|
||||
<li>Providers/DeepInfra: add a bundled DeepInfra provider with <code>DEEPINFRA_API_KEY</code> onboarding, dynamic OpenAI-compatible model discovery, image generation/editing, image/audio media understanding, TTS, text-to-video, memory embeddings, static catalog metadata, and provider-owned base URL policy. Carries forward #53805, #48088, #37576, #43896, #11533, and #2554. Thanks @ats3v.</li>
|
||||
<li>Matrix: attach versioned structured approval metadata to pending approval messages so capable Matrix clients can render richer approval UI while body text and reaction fallback keep working. (#72432) Thanks @kakahu2015.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Gateway/sessions: align <code>chat.history</code> and <code>sessions.list</code> thinking defaults with owning-agent and catalog-aware resolution so Control UI session defaults match backend runtime state. (#63418) Thanks @jpreagan.</li>
|
||||
<li>Devices/pairing: recover array-shaped device and node pairing state files before persisting approvals, so UUID-keyed pending and paired entries no longer disappear after a malformed JSON store write. Fixes #63035. Thanks @sar618.</li>
|
||||
<li>Gateway/auth: clear reused stale device tokens and stop reconnecting on device-token mismatch in the Control UI and Node gateway clients, avoiding rate-limit loops after scope-upgrade or token-rotation handoffs. Fixes #71609. Thanks @ricksayhi.</li>
|
||||
<li>Gateway/approvals: treat duplicate same-decision approval resolves as idempotent during the resolved-entry grace window, including consumed <code>allow-once</code> approvals, while returning an explicit already-resolved error for conflicting repeats. Fixes #59162; refs #58479 and #65486. Thanks @wikithoughts, @sajazuniga7-coder, and @mjmai20682068-create.</li>
|
||||
<li>Channels/Telegram: honor <code>approvals.exec/plugin.targets[].accountId</code> when routing native approvals across multi-bot Telegram accounts while preserving unscoped Telegram targets for any account. Fixes #69916. Thanks @joerod26.</li>
|
||||
<li>Telegram/gateway: bound outbound Bot API calls and cache bundled plugin alias lookup so slow Telegram sends or WSL2 filesystem scans no longer wedge gateway replies. (#74210) Thanks @obviyus.</li>
|
||||
<li>Agents/exec: omit the internal session-resume fallback preface from successful async exec completion messages sent directly back to chat. Fixes #67181. Thanks @raistlin88.</li>
|
||||
<li>Agents/media: register detached <code>video_generate</code> and <code>music_generate</code> tool run contexts until terminal status, so Discord-backed provider jobs stay live in <code>/tasks</code> instead of becoming <code>lost</code> when the parent chat run context disappears. Thanks @vincentkoc.</li>
|
||||
<li>Agents/media: prefer OpenAI image and video providers when the default model uses the OpenAI Codex auth alias, so auto media generation no longer falls through to Fal before GPT Image or Sora. Thanks @vincentkoc.</li>
|
||||
<li>Tasks/media: infer agent ownership for session-scoped task records so <code>/tasks</code> agent-local fallback includes session-backed <code>video_generate</code> and other async media jobs even when the current chat session has no linked rows. Thanks @vincentkoc.</li>
|
||||
<li>Agents/media: keep long-running <code>video_generate</code> and <code>music_generate</code> tasks fresh while provider jobs are still pending, so task maintenance does not mark active Discord media renders lost before completion. Thanks @vincentkoc.</li>
|
||||
<li>CLI/status: treat scope-limited gateway probes as reachable-but-degraded in shared status scans, so <code>openclaw status --all</code> no longer reports a live gateway as unreachable after <code>missing scope: operator.read</code>. Fixes #49180; supersedes #47981. Thanks @openjay.</li>
|
||||
<li>CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007.</li>
|
||||
<li>Slack/Socket Mode: use a 15s Slack SDK pong timeout by default and add <code>channels.slack.socketMode.clientPingTimeout</code>, <code>serverPingTimeout</code>, and <code>pingPongLoggingEnabled</code> overrides so stale-websocket handling no longer depends on app-event health heuristics. Fixes #14248; refs #58519, #64009, and #63488. Thanks @shivasymbl and @freerk.</li>
|
||||
<li>Slack/media: bound private file and forwarded attachment downloads with idle and total timeouts while preserving placeholder fallback, so stalled Slack <code>file_share</code> media no longer wedges inbound message handling. Fixes #61850. Thanks @bassboy2k.</li>
|
||||
<li>Plugins/inspector: keep bundled plugin runtime capture quiet and config-tolerant for Codex, memory-lancedb, Feishu, Mattermost, QQBot, and Tlon so plugin-inspector JSON checks can validate the full bundled set. Thanks @vincentkoc.</li>
|
||||
<li>Slack/auto-reply: keep fully consumed text reset triggers such as <code>new session</code> out of <code>BodyForAgent</code> after directive cleanup, so configured Slack reset phrases do not leak into the fresh model turn. Fixes #73137. Thanks @neeravmakwana.</li>
|
||||
<li>Plugins/runtime deps: prune stale retained bundled runtime deps and keep doctor/secret channel contract scans on lightweight artifacts, so disabled bundled channels stop preserving old dependency trees or importing heavy plugin surfaces. Thanks @SymbolStar and @vincentkoc.</li>
|
||||
<li>Plugins/runtime deps: cache unchanged bundled runtime mirror dist-file materialization decisions and close file-lock handles on owner-write failures, reducing repeated startup chunk scans and avoiding FileHandle-GC recovery stalls. Refs #73532. Thanks @oadiazp and @bstanbury.</li>
|
||||
<li>Auto-reply: bound the post-run pending tool-result delivery drain with a progress-aware idle timeout, so a never-settling tool-result task no longer leaves the session active forever while slow healthy deliveries can keep draining. Fixes #53889; supersedes #64733 and #73434. Thanks @zijunl and @wujiaming88.</li>
|
||||
<li>Gateway/startup: start chat channels without waiting for primary model prewarm, keeping model warmup bounded in the background so Slack and other channels come online promptly when provider discovery is slow. Supersedes #73420. Thanks @dorukardahan.</li>
|
||||
<li>Gateway/install: carry env-backed config SecretRefs such as <code>channels.discord.token</code> into generated service environments when they are present only in the installing shell, while keeping gateway auth SecretRefs non-persisted. Fixes #67817; supersedes #73426. Thanks @wdimaculangan and @ztexydt-cqh.</li>
|
||||
<li>Auto-reply/commands: stop bare <code>/reset</code> and <code>/new</code> after reset hooks acknowledge the command, so non-ACP channels no longer fall through into empty provider calls while <code>/reset <message></code> and <code>/new <message></code> still seed the next model turn. Fixes #73367 and #73412. Thanks @hoyanhan, @wenxu007, and @amdhelper.</li>
|
||||
<li>Providers/DeepSeek: backfill DeepSeek V4 <code>reasoning_content</code> on plain assistant replay messages as well as tool-call turns, so thinking sessions with prior tool use no longer fail follow-up requests with missing reasoning content. Fixes #73417; refs #71372. Thanks @34262315716 and @Bartok9.</li>
|
||||
<li>Agents/gateway tool: strip full config payloads from <code>config.patch</code> and <code>config.apply</code> tool responses while preserving direct RPC responses, so config-heavy sessions no longer replay large redacted configs into transcript history. Fixes #47610; supersedes #73439. Thanks @HanenVit and @juan-flores077.</li>
|
||||
<li>Auto-reply: preserve voice-note media from silent turns while continuing to suppress text and non-voice media, so <code>NO_REPLY</code> TTS replies still deliver the requested audio bubble. (#73406) Thanks @zqchris.</li>
|
||||
<li>Channels/Mattermost: stop enqueueing regular inbound posts as system events, so Mattermost user messages reach the model only as user-role inbound-envelope content instead of also appearing as <code>System: Mattermost message...</code> directives. Fixes #71795. Thanks @juan-flores077.</li>
|
||||
<li>Agents/media: qualify bare <code>agents.defaults.imageModel</code> and <code>pdfModel</code> refs from unique configured image-capable providers, so Ollama vision models such as <code>moondream</code> and <code>qwen2.5vl:7b</code> do not fall through to the default provider. Fixes #38816; supersedes #73396. Thanks @alainasclaw and @vincentkoc.</li>
|
||||
<li>Agents/Anthropic: send implicit Anthropic beta headers only to direct public Anthropic endpoints, including OAuth, so custom Anthropic-compatible providers no longer mis-handle unsupported beta flags unless explicitly configured. Refs #73346. Thanks @byBrodowski.</li>
|
||||
<li>Skills: require explicit <code>skills.entries.coding-agent.enabled</code> before exposing the bundled coding-agent skill, so installs with Codex on PATH but no OpenAI auth do not silently offer Codex delegation. Fixes #73358. Thanks @LaFleurAdvertising and @Sanjays2402.</li>
|
||||
<li>Plugins/startup: treat manifestless Claude bundles as valid installed-plugin registry entries instead of stale missing manifests, so workspace bundles no longer force repeated derived registry rebuilds or noisy <code>plugins.entries.workspace</code> warnings during Gateway startup. Fixes #73433. Thanks @AnneVoss.</li>
|
||||
<li>Agents/subagents: preserve <code>sessions_yield</code> as a paused subagent state and ignore its wait text while freezing completion output, so parent sessions wait for the final post-compaction answer instead of receiving intermediate progress or <code>(no output)</code>. Fixes #73413. Thanks @Ask-sola.</li>
|
||||
<li>Plugins/startup: precompute bundled runtime mirror fingerprints before taking the mirror lock and keep Docker bundled plugin runtime deps/mirrors in a Docker-managed volume instead of the Windows/WSL config bind mount, so cold starts avoid slow host-volume mirror writes. Fixes #73339. Thanks @1yihui.</li>
|
||||
<li>Plugins/runtime deps: refresh bundled runtime mirrors without deleting active import trees, so config-triggered restarts do not see transient missing plugin files during registration. Thanks @shakkernerd.</li>
|
||||
<li>Channels/LINE: persist inbound image, video, audio, and file downloads in <code>~/.openclaw/media/inbound/</code> instead of temporary files so agents can still read LINE media after <code>/tmp</code> cleanup. Fixes #73370. Thanks @hijirii and @wenxu007.</li>
|
||||
<li>CLI/plugins: keep bundled plugin installs out of <code>plugins.load.paths</code> while preserving install records, so install/inspect/doctor loops no longer warn about the current bundled plugin directory. Thanks @vincentkoc.</li>
|
||||
<li>CLI/plugins: scope <code>plugins inspect <id></code> runtime loading to the matched plugin so single-plugin inspection does not load every plugin before checking the target. Thanks @shakkernerd.</li>
|
||||
<li>CLI/plugins: remove managed copied-path plugin directories during uninstall and plan uninstall from metadata instead of runtime-loading plugins, so plugin lifecycle commands avoid unnecessary bundled runtime-deps work. Thanks @shakkernerd.</li>
|
||||
<li>Cron tool: infer the creating session's agentId for <code>cron.add</code> jobs when <code>agentId</code> is omitted or passed as undefined, keeping scheduled agentTurn jobs routed to the session agent; #40571 identified the guard bug and supplied the focused regression coverage. Thanks @ChanningYul.</li>
|
||||
<li>Cron/Telegram: add <code>--thread-id</code> to <code>openclaw cron add</code> and <code>openclaw cron edit</code>, preserving Telegram forum topic delivery targets across scheduled announcements. Carries forward #51581, #60373, and #60890. Thanks @ChunHao-dev.</li>
|
||||
<li>Cron/Telegram: preserve session-derived Telegram topic thread IDs when isolated cron delivery explicitly targets the parent chat, keeping bare chat targets in the active forum topic without leaking stale topics to other chats. Carries forward #64708. Thanks @addelh.</li>
|
||||
<li>Memory/compaction: keep pre-compaction memory-flush prompts runtime-only so session transcripts and <code>chat.history</code> no longer expose them as normal user turns. Fixes #54408 and #58956; refs #43567. Thanks @markgong and @guoyuhang9.</li>
|
||||
<li>Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger <code>RangeError: Maximum call stack size exceeded</code>. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk.</li>
|
||||
<li>Agents/Anthropic: cancel stalled Anthropic Messages SSE body reads when abort signals fire, so active-memory timeouts release transport resources instead of leaving hidden recall runs parked on <code>reader.read()</code>. Refs #72965 and #73120. Thanks @wdeveloper16.</li>
|
||||
<li>Control UI/WebChat: keep pending run and typing state attached to the active client run, so unowned inject/announce/side-result finals no longer unlock unrelated active runs while completed owned runs still clear promptly. Fixes #57795; carries forward the narrow diagnosis from #57887. Thanks @haoyu-haoyu.</li>
|
||||
<li>Sandbox/Docker: stop satisfying a missing default sandbox image by tagging plain Debian as <code>openclaw-sandbox:bookworm-slim</code>, preserving the Python tooling required by sandbox write/edit helpers and directing users to build the default image. Fixes #51185; refs #45108, #51099, #51609, and #57713. Thanks @dpalis, @Tin55FoilDev, @jbcohen2-coder, @macminihal-cyber, and @PraxoOnline.</li>
|
||||
<li>Control UI/WebChat: confirm toolbar New Session button resets before dispatching <code>/new</code> while leaving typed <code>/new</code> and <code>/reset</code> commands immediate. Fixes #45800; refs #27065, #56611, #54499, and #27110. Thanks @aethnova, @kosta228-huli, @adambezemek, and @xss925175263 (xianshishan).</li>
|
||||
<li>Agents/models: keep per-agent primary models strict when <code>fallbacks</code> is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.</li>
|
||||
<li>Gateway/models: add <code>models.pricing.enabled</code> so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.</li>
|
||||
<li>Gateway/startup: warn when legacy <code>CLAWDBOT_*</code> or <code>MOLTBOT_*</code> environment variables are still present, pointing users to <code>OPENCLAW_*</code> names instead of failing silently. Fixes #53482; carries forward #53667. Thanks @lndyzwdxhs.</li>
|
||||
<li>Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale <code>OPENCLAW_GATEWAY_TOKEN</code> or <code>OPENCLAW_GATEWAY_PASSWORD</code> values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.</li>
|
||||
<li>Doctor/state: require an interactive confirmation before archiving orphan transcript files, so <code>openclaw doctor --fix</code> no longer silently renames recoverable session history after upgrades regenerate <code>sessions.json</code>. Fixes #73106. Thanks @scottgl9.</li>
|
||||
<li>Cron/Telegram: preserve explicit <code>:topic:</code> delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.</li>
|
||||
<li>Build/runtime: write the runtime-postbuild stamp after <code>pnpm build</code> writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.</li>
|
||||
<li>Build/runtime: preserve staged bundled-plugin runtime dependency caches across source-checkout tsdown rebuilds, so local CLI and gateway-watch rebuilds no longer recreate large plugin dependency trees before starting. Refs #73205. Thanks @SymbolStar.</li>
|
||||
<li>CLI/channels: list configured chat channel accounts from read-only setup metadata even when the standalone CLI has not loaded the runtime channel registry, so <code>openclaw channels list</code> shows Telegram accounts before auth providers. Fixes #73319 and #73322. Thanks @mlaihk.</li>
|
||||
<li>CLI/model probes: keep <code>infer model run --gateway</code> raw by skipping prior session transcript, bootstrap context, context-engine assembly, tools, and bundled MCP servers, so local backends can be tested without full agent-context overhead. Fixes #73308. Thanks @ScientificProgrammer.</li>
|
||||
<li>CLI/image describe: pass <code>--prompt</code> and <code>--timeout-ms</code> through <code>infer image describe</code> and <code>describe-many</code>, so custom vision instructions and slow local model budgets reach media-understanding providers such as Ollama, OpenAI, Google, and OpenRouter. Addresses #63700. Thanks @cedricjanssens.</li>
|
||||
<li>Providers/Ollama: reject long non-linguistic Kimi/GLM symbol runs as provider failures instead of storing them as successful visible assistant replies, so fallback or error handling can recover from garbled cloud output. Fixes #64262; refs #67019. Thanks @Kloz813 and @xiaomenger123.</li>
|
||||
<li>CLI/model probes: reject empty or whitespace-only <code>infer model run --prompt</code> values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge.</li>
|
||||
<li>Gateway/media: route text-only <code>chat.send</code> image offloads through media-understanding fields so <code>agents.defaults.imageModel</code> can describe WebChat attachments instead of leaving only an opaque <code>media://inbound</code> marker. Fixes #72968. Thanks @vorajeeah.</li>
|
||||
<li>Gateway/Windows: route no-listener restart handoffs through the Windows supervisor without leaving restart tokens in flight, so failed task scheduling can be retried and successful handoffs do not coalesce later restart requests. (#69056) Thanks @Thatgfsj.</li>
|
||||
<li>Gateway/model pricing: skip plugin manifest discovery during background pricing refreshes when <code>plugins.enabled: false</code>, so disabled-plugin setups do not keep rebuilding plugin metadata from the Gateway hot path. Fixes #73291. Thanks @slideshow-dingo and @fishgills.</li>
|
||||
<li>Ollama/thinking: validate <code>/think</code> commands against live Ollama catalog reasoning metadata and preserve explicit native <code>params.think</code>/<code>params.thinking</code>, so models whose <code>/api/show</code> capabilities include <code>thinking</code> expose <code>low</code>, <code>medium</code>, <code>high</code>, and <code>max</code> instead of being stuck on <code>off</code>. Fixes #73366. Thanks @cymise.</li>
|
||||
<li>Gateway/sessions: remove automatic oversized <code>sessions.json</code> rotation backups, deprecate <code>session.maintenance.rotateBytes</code>, and teach <code>openclaw doctor --fix</code> to remove the ignored key so hot session writes no longer copy multi-MB stores. Refs #72338. Thanks @midhunmonachan and @DougButdorf.</li>
|
||||
<li>Channels/Telegram: fail fast when Telegram rejects the startup <code>getMe</code> token probe with 401, so invalid or stale BotFather tokens are reported as token auth failures instead of misleading <code>deleteWebhook</code> cleanup failures. Fixes #47674. Thanks @samaedan-arch.</li>
|
||||
<li>ACPX: keep generated Codex and Claude ACP wrapper startup paths working when remote or special state filesystems reject chmod, since OpenClaw invokes the wrappers through Node instead of executing them directly. Fixes #73333. Thanks @david-garcia-garcia.</li>
|
||||
<li>CLI/onboarding: infer image input for common custom-provider vision model IDs, ask only for unknown models, and keep <code>--custom-image-input</code>/<code>--custom-text-input</code> overrides so vision-capable proxies do not get saved as text-only configs. Fixes #51869. Thanks @Antsoldier1974.</li>
|
||||
<li>Models/OpenAI Codex: stop listing or resolving unsupported <code>openai-codex/gpt-5.4-mini</code> rows through Codex OAuth, keep stale discovery rows suppressed with a clear API-key-route hint, and leave direct <code>openai/gpt-5.4-mini</code> available. Fixes #73242. Thanks @0xCyda.</li>
|
||||
<li>Plugin SDK: restore the root <code>stringEnum</code> and <code>optionalStringEnum</code> exports on both the published SDK entry and runtime root-alias bridge, so older external plugins can keep building and loading while migrating to focused SDK subpaths. Fixes #68279. Thanks @marzliak.</li>
|
||||
<li>Plugin SDK: restore the root-alias bridge for <code>registerContextEngine</code> and expose missing legacy compat helpers <code>normalizeAccountId</code> and <code>resolvePreferredOpenClawTmpDir</code> so older external plugins such as <code>openclaw-weixin</code> can keep loading while migrating to focused SDK subpaths. Fixes #53497. Thanks @alanxchen85.</li>
|
||||
<li>Auth profiles: make <code>openclaw doctor --fix</code> migrate legacy flat <code>auth-profiles.json</code> files such as <code>{ "ollama-windows": { "apiKey": "ollama-local" } }</code> to canonical provider default API-key profiles with a backup, so custom Ollama/OpenAI-compatible providers recover cleanly after upgrading. Fixes #59629; supersedes #59642. Thanks @Xsanders555 and @Linux2010.</li>
|
||||
<li>Memory/Dreaming: retry Dream Diary once with the session default when a configured dreaming model is unavailable, while leaving subagent trust and allowlist errors visible instead of silently masking configuration problems. Refs #67409 and #69209. Thanks @Ghiggins18 and @everySympathy.</li>
|
||||
<li>Feishu/inbound files: recover CJK filenames from plain <code>Content-Disposition: filename=</code> download headers when Feishu exposes UTF-8 bytes through Latin-1 header decoding, while leaving valid Latin-1 and JSON-derived names unchanged. (#48578, #50435, #59431) Thanks @alex-xuweilong, @lishuaigit, and @DoChaoing.</li>
|
||||
<li>Channels/Telegram: normalize accidental full <code>/bot<TOKEN></code> Telegram <code>apiRoot</code> values at runtime and teach <code>openclaw doctor --fix</code> to remove the suffix, so startup control calls no longer 404 when direct Bot API curl commands work. Fixes #55387. Thanks @brendanmatthewjones-cmyk, @techfindubai-ux, and @Sivlerback-Chris.</li>
|
||||
<li>Zalo Personal: persist refreshed <code>zca-js</code> session cookies after QR login, session restore, and successful API calls so gateway restarts restore the freshest local session. (#73277) Thanks @darkamenosa.</li>
|
||||
<li>Logging/security: redact sensitive tokens (sk-\* keys, Bearer/Authorization values, etc.) at the subsystem console sink so <code>createSubsystemLogger().info/warn/error</code> output that bypasses the patched console-capture handler still applies the same redaction the file transport already does. Fixes #73284; refs #67953 and #64046. Thanks @edwin-rivera-dev.</li>
|
||||
<li>Plugins/runtime deps: reuse enclosing versioned cache roots when bundled plugins resolve from nested staged paths, so plugin-runtime-deps no longer mints <code>openclaw-unknown-*</code> directories or loops on <code>ENOTEMPTY</code>. Fixes #72956. (#73205) Thanks @SymbolStar.</li>
|
||||
<li>Agents/failover: classify CJK provider transport, quota, billing, auth, and overload error text so Chinese-language provider failures trigger fallback and user-facing transport copy instead of surfacing as unclassified raw errors. (#56242) Thanks @tomcatzh.</li>
|
||||
<li>Agents/failover: seed non-claude-cli fallback prompts with Claude Code session context when a claude-cli attempt fails, so fallback models do not restart cold after billing or quota failover. (#72069) Thanks @stainlu.</li>
|
||||
<li>Agents/CLI runner: transfer bundle-MCP tempDir cleanup from the per-turn runner finally to the Claude live-session lifecycle, so persistent Claude CLI sessions keep their <code>--mcp-config</code> directory until the live subprocess closes. Fixes #73244. Thanks @edwin-rivera-dev.</li>
|
||||
<li>Gateway/nodes: allow Windows companion nodes to use safe declared commands such as canvas, camera list, location, device info, and screen snapshot by default while keeping dangerous media commands opt-in. (#71884) Thanks @shanselman.</li>
|
||||
<li>Agents/cron: clarify agent-tool and CLI cron timezone guidance so supplied <code>tz</code> values use local wall-clock cron fields and omitted cron <code>tz</code> falls back to the Gateway host local timezone. Fixes #53669; carries forward #46177. (#73372) Thanks @chen-zhang-cs-code and @maranello-o.</li>
|
||||
<li>Providers/Qwen: allow explicitly configured <code>qwen/qwen3.6-plus</code> to resolve on Qwen Coding Plan endpoints while keeping the built-in catalog from advertising it there. Fixes #63654; carries forward #63987. Thanks @jepson-liu.</li>
|
||||
<li>Channels/Telegram: keep Bot API network fallbacks sticky after failed attempts and retry timed-out startup control calls once on the fallback route, so <code>deleteWebhook</code> IPv6 stalls no longer trigger slow multi-account retry storms. Fixes #73255. Thanks @ttomiczek and @sktbrd.</li>
|
||||
<li>Gateway/agents: accept heartbeat, cron, and webhook as internal channel hints for agent runs so <code>sessions_spawn</code> works from non-delivery parent sessions while unknown channel hints still fail closed. Fixes #73237. Thanks @KeWang0622.</li>
|
||||
<li>Gateway/models: merge explicit <code>models.providers.*.models</code> rows into the Gateway model catalog with normalized provider/model dedupe, and use normalized image-capability lookup so custom vision models keep native image attachments even when Pi discovery omits them or model ID casing differs. Fixes #64213 and #65165. Thanks @billonese and @202233a.</li>
|
||||
<li>Gateway/reload: publish canonical post-write source config to in-process reloaders so simple config saves no longer create phantom plugin diffs or trigger unnecessary Gateway restarts. (#73267) Thanks @szsip239.</li>
|
||||
<li>Gateway/Docker: keep config-triggered restarts in-process inside containers instead of spawning a detached child and exiting PID 1 cleanly, so Docker Swarm and other on-failure supervisors do not leave the service stuck at 0/1 replicas. Fixes #73178. Thanks @du-nguyen-IT007.</li>
|
||||
<li>CLI/tasks: ship the task-registry control runtime in npm packages so <code>openclaw tasks cancel</code> can load ACP/subagent cancellation helpers from published builds. Fixes #68997. Thanks @1OAKDesign.</li>
|
||||
<li>Channels/Telegram: preserve unsent generated media after partial reply streaming has already delivered the text, so <code>image_generate</code> outputs still reach Telegram as photos instead of being dropped from the final payload. Fixes #73253. Thanks @mlaihk.</li>
|
||||
<li>Memory-core/dreaming: cap detached Dream Diary narrative subagents across cron sweeps so multi-workspace dreaming no longer fans out unbounded subagent sessions, lock contention, and cascading narrative timeouts. Fixes #73198. (#73287) Thanks @KeWang0622.</li>
|
||||
<li>CLI/agents: close local one-shot Claude live stdio sessions and bundled MCP loopback resources after embedded <code>openclaw agent --local</code> runs, while keeping gateway-owned MCP loopback cleanup internal to the Gateway. Thanks @frankekn.</li>
|
||||
<li>Export/session: keep inline export HTML scripts and vendor libraries injected after template formatting so generated session exports open with the app code, markdown renderer, and syntax highlighter present. Fixes #41862 and #49957; carries forward #41861 and #68947. Thanks @briannewman, @martenzi, and @armanddp.</li>
|
||||
<li>Agents/ACPX: stage the patched Claude ACP adapter as an ACPX runtime dependency and route known Codex/Claude ACP commands through local wrappers, so Gateway runtime no longer depends on live <code>npx</code> adapter resolution. Fixes #73202. Thanks @joerod26.</li>
|
||||
<li>Memory/compaction: let pre-compaction memory flush use an exact <code>agents.defaults.compaction.memoryFlush.model</code> override such as <code>ollama/qwen3:8b</code> without inheriting the active session fallback chain, so local housekeeping can avoid paid conversation models. Fixes #53772. Thanks @limen96.</li>
|
||||
<li>macOS/update: stop managed Gateway services before package replacement and keep LaunchAgent service secrets out of world-readable plist metadata by loading them from owner-only env files. Fixes #72996. Thanks @Mathewb7.</li>
|
||||
<li>Google Meet: keep observe-only Chrome joins and setup checks from requiring BlackHole or audio bridge commands, avoid granting or selecting the microphone in observe-only mode, and make <code>test_speech</code> report fresh realtime output-byte verification instead of only confirming a queued utterance. Refs #72478. Thanks @DougButdorf.</li>
|
||||
<li>Gateway/hooks: route non-delivered hook completion and error summaries to the target agent's main session instead of the default agent session, preserving multi-agent hook isolation. Fixes #24693; carries forward #68667. Thanks @abersonFAC and @bluesky6868.</li>
|
||||
<li>Control UI/models: request the configured Gateway model-list view so dashboards with only <code>models.providers.*.models</code> show those configured models first instead of flooding the picker with the full built-in catalog. Fixes #65405. Thanks @wbyanclaw.</li>
|
||||
<li>CLI/models: keep default-model and allowlist pickers on explicit <code>models.providers.*.models</code> entries when <code>models.mode</code> is <code>replace</code> instead of loading the full built-in catalog. Fixes #64950. Thanks @mrozentsvayg.</li>
|
||||
<li>Media/security: tighten media-understanding MIME sanitization so parameterized MIME values stay end-anchored and malformed whitespace or suffix payloads are rejected before file-context handling. Fixes #9795; carries forward #68225 with related review/test context from #61016/#68456. Thanks @ymaxgit, @bluesky6868, and @shamsulalam1114.</li>
|
||||
<li>Discord: own the Carbon interaction listener and hand off Discord slash/component handling asynchronously, so compaction or long session locks no longer trip <code>InteractionEventListener</code> listener timeouts. Fixes #73204. Thanks @slideshow-dingo.</li>
|
||||
<li>Compaction/diagnostics: keep unknown compaction failure classifications stable while logging sanitized detail for unclassified provider errors such as missing Ollama provider adapters. Thanks @gzsiang.</li>
|
||||
<li>Models/fallbacks: record first-class <code>model.fallback_step</code> trajectory events with from/to models, failure detail, chain position, and final outcome so support exports preserve the primary model failure even when a later fallback also fails. Fixes #71744. Thanks @nikolaykazakovvs-ux.</li>
|
||||
<li>Gateway/agents: block agent <code>exec</code> from launching interactive <code>openclaw channels login</code> flows and abort active agent runs after invalid-config recovery restores last-known-good config, preventing known channel-login and reload paths from wedging replies. Refs #72338. Thanks @midhunmonachan.</li>
|
||||
<li>Gateway/diagnostics: emit payload-free liveness warnings with event-loop delay, event-loop utilization, CPU-core ratio, active-session counts, and OTEL warning metrics/spans so live-but-stalled Gateways capture CPU-spin context in stability bundles and telemetry. Refs #72338. Thanks @midhunmonachan and @DougButdorf.</li>
|
||||
<li>Gateway/startup: keep value-option foreground starts on the gateway fast path and skip proxy bootstrap unless proxy env is configured, reducing normal gateway startup RSS and avoiding full CLI graph loading. Thanks @vincentkoc.</li>
|
||||
<li>Heartbeat/models: show heartbeat model bleed guidance on context-overflow resets when the last runtime model matches configured <code>heartbeat.model</code>, so smaller local heartbeat models point users to <code>isolatedSession</code> or <code>lightContext</code> instead of only compaction-buffer tuning. Fixes #67314. Thanks @Knightmare6890.</li>
|
||||
<li>Subagents/models: persist <code>sessions_spawn.model</code> and configured subagent models as child-session model overrides before the first turn, so spawned subagents actually run on the requested provider/model instead of reverting to the target agent default. Fixes #73180. Thanks @danielzinhu99.</li>
|
||||
<li>Channels/Telegram: keep webhook-mode local listeners alive and retry Telegram <code>setWebhook</code> registration after recoverable startup network failures, so transient Bot API timeouts no longer leave reverse proxies pointing at a closed listener. Fixes #71834. Thanks @jinon86.</li>
|
||||
<li>Agents/ACPX: bundle the Codex ACP adapter and launch it from the isolated <code>CODEX_HOME</code> wrapper before falling back to npm, so Codex ACP startup no longer depends on live <code>npx</code> resolution or the stale <code>@zed-industries/codex-acp@^0.11.1</code> range. Fixes #72037; refs #73202. Thanks @jasonftl, @sazora, and @joerod26.</li>
|
||||
<li>Agents/ACPX: register the embedded ACP backend at Gateway startup through a lightweight ACP backend SDK path and without importing the heavy ACPX runtime until an ACP session or explicit startup probe needs it, reducing baseline Gateway RSS. Thanks @vincentkoc.</li>
|
||||
<li>CLI/update: keep restart health polling when the restarted Gateway is reachable but has not reported its version yet, so macOS service restarts do not fail early with <code>actual unavailable</code>. Thanks @ProspectOre.</li>
|
||||
<li>Backup: skip installed plugin <code>extensions/*/node_modules</code> dependency trees while keeping plugin manifests and source files in archives, so local backups avoid rebuildable npm payload bloat. Fixes #64144. Thanks @BrilliantWang.</li>
|
||||
<li>Cron/models: fail isolated cron runs closed when an explicit <code>payload.model</code> is not allowed or cannot be resolved, so scheduled jobs do not silently fall back to an unrelated agent default or paid route before configured provider proxies such as LiteLLM can run. Fixes #73146. Thanks @oneandrewwang.</li>
|
||||
<li>Memory/QMD: back off repeated chat-turn QMD open failures while still letting memory status and CLI probes recheck immediately, so a broken sidecar dependency cannot trigger active-memory or cron retry storms. Fixes #73188 and #73176. Thanks @leonlushgit and @w3i-William.</li>
|
||||
<li>Talk Mode: resolve <code>messages.tts.providers.<id>.apiKey</code> through the active runtime snapshot for <code>talk.config</code>, so Talk overlays can discover SecretRef-backed speech providers without falling back to local speech. Fixes #73109. (#73111) Thanks @omarshahine.</li>
|
||||
<li>Memory/Ollama: resolve <code>memorySearch.provider</code> custom provider ids through their configured <code>models.providers.<id>.api</code> owner, so multi-GPU Ollama setups can dedicate embeddings to providers such as <code>ollama-5080</code> without losing the Ollama adapter or local auth semantics. Fixes #73150. Thanks @oneandrewwang.</li>
|
||||
<li>CLI/memory: skip eager context-window warmup for <code>openclaw memory</code> commands so memory search does not race unrelated model metadata discovery. Fixes #73123. Thanks @oalansilva and @neeravmakwana.</li>
|
||||
<li>CLI/Telegram: route Telegram <code>message send</code> and poll actions through the running Gateway when available, so packaged installs use the staged <code>grammy</code> runtime deps and CLI sends return instead of hanging after the Telegram channel is active. Fixes #73140. Thanks @oalansilva.</li>
|
||||
<li>Plugins/runtime deps: prepare staged bundled plugin dependencies before loading packaged public surfaces, so OpenClaw's Telegram runtime/test facade loads resolve <code>grammy</code> from the managed runtime-deps stage without copying dependencies into the global package root. Refs #73140. Thanks @oalansilva.</li>
|
||||
<li>Agents/exec: emit <code>(no output)</code> for silent exec update and node-host result blocks so Anthropic-compatible providers no longer reject empty tool-result text after quiet commands. Fixes #73117. Thanks @pfrederiksen and @Sanjays2402.</li>
|
||||
<li>Cron/providers: preflight local Ollama and OpenAI-compatible provider endpoints before isolated cron agent turns, record unreachable local providers as skipped runs, and cache dead-endpoint probes so many jobs do not hammer the same stopped local server. Fixes #58584. Thanks @jpeghead.</li>
|
||||
<li>Gateway/config: let config reload continue in degraded mode when invalidity is scoped to plugin entries, so incompatible plugin configs can be skipped and the Gateway restart can still pick up the rest of the config after rollbacks. Fixes #73131. Thanks @Adam-Researchh.</li>
|
||||
<li>Doctor/channels: suppress disabled bundled-plugin blocker warnings when a trusted external plugin owns the configured channel, so Lark/Feishu installs no longer get Feishu repair noise after switching to <code>openclaw-lark</code>. Fixes #56794. Thanks @wuji-tech-dev.</li>
|
||||
<li>CLI/status: show skipped fast-path memory checks as <code>not checked</code> and report active custom memory plugin runtime status from <code>status --json --all</code> without requiring built-in <code>agents.defaults.memorySearch</code>, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius.</li>
|
||||
<li>Gateway/channels: record and log unexpected clean channel monitor exits so channels that return without throwing no longer appear stopped with no error. Fixes #73099. Thanks @balaji1968-kingler.</li>
|
||||
<li>Discord/group chats: keep group/channel replies private by default unless the agent explicitly uses the message tool, so always-on rooms can lurk without leaking automatic final, block, preview, or status-reaction output; <code>messages.groupChat.visibleReplies: "automatic"</code> restores legacy auto-posting. (#73046) Thanks @scoootscooob.</li>
|
||||
<li>Plugins/package: force nested bundled-plugin runtime dependency installs out of inherited npm dry-run mode during prepack and package smoke checks, so packed installs materialize required plugin modules instead of reporting missing bundled files. Refs #73128. Thanks @Adam-Researchh.</li>
|
||||
<li>Discord: skip reaction events before REST channel fetch when notifications are off, guild reactions are disabled, or allowlist mode cannot match without channel overrides, reducing reconnect bursts that caused slow listener warnings. Fixes #73133. Thanks @isaacsummers.</li>
|
||||
<li>Channels/Telegram: centralize polling update tracking so accepted offsets remain durable across restarts, same-process handler failures can still retry, and slow offset writes cannot overwrite newer accepted watermarks. Refs #73115. Thanks @vdruts.</li>
|
||||
<li>Agents/models: classify empty, reasoning-only, and planning-only terminal agent runs before accepting a model fallback candidate, so invalid or incompatible models can advance to the next configured fallback instead of returning a 30-second terminal failure. Fixes #73115. Thanks @vdruts.</li>
|
||||
<li>Memory/LanceDB: let embedding config use provider-backed auth profiles, environment credentials, or provider config without a separate plugin <code>embedding.apiKey</code>, so OAuth-capable embedding providers can power auto-recall/capture. Fixes #68950. Thanks @malshaalan-ai.</li>
|
||||
<li>CLI/parents: invoking <code>openclaw <parent></code> (memory, channels, plugins, approvals, devices, cron, mcp) without a subcommand now prints the parent's help and exits <code>0</code>, matching <code><parent> --help</code> and the existing <code>agents</code> / <code>sessions</code> defaults so shell <code>&&</code> chains and pnpm wrappers no longer surface a misleading <code>ELIFECYCLE Command failed with exit code 1.</code> line. Fixes #73077. Thanks @hclsys.</li>
|
||||
<li>Plugins/hooks: time out never-settling <code>agent_end</code> observation hooks after 30 seconds and log the plugin failure, so hung embedding endpoints no longer leave memory capture silently pending forever. Fixes #65544. Thanks @ghoc0099.</li>
|
||||
<li>Gateway/config: serve runtime config schemas from the current plugin metadata snapshot and generated bundled channel schema metadata instead of rebuilding plugin channel config modules on every <code>config.get</code>/<code>config.schema</code>, preventing idle plugin-discovery CPU churn after upgrades. Fixes #73088. Thanks @sleitor and @geovansb.</li>
|
||||
<li>Memory/LanceDB: call OpenAI-compatible embedding endpoints through the raw SDK transport without sending <code>encoding_format</code>, then normalize float-array or base64 responses so providers such as ZhiPu and DashScope no longer fail recall with wrong vector dimensions or rejected parameters. Fixes #63655. Thanks @kinthaiofficial.</li>
|
||||
<li>Plugins/install: run dependency installs with npm error-level logging instead of silent mode so failed plugin or hook installs surface actionable npm errors such as EUNSUPPORTEDPROTOCOL instead of <code>npm install failed:</code> with no detail. (#73093) Thanks @sanctrl.</li>
|
||||
<li>Memory/LanceDB: bound memory recall embedding queries with a new <code>recallMaxChars</code> setting, prefer the latest user message over channel prompt metadata during auto-recall, and document the knob so small Ollama embedding models avoid context-length failures. Fixes #56780. Thanks @rungmc357 and @zak-collaborator.</li>
|
||||
<li>CLI/skills: resolve workspace-backed skills commands from <code>--agent</code>, then the current agent workspace, before falling back to the default agent, so multi-agent ClawHub installs, updates, and status checks stay scoped to the active workspace. Fixes #56161; carries forward #72726. Thanks @langbowang and @luyao618.</li>
|
||||
<li>Plugin SDK: fall back from partial bundled plugin directory overrides to package source public surfaces while preserving <code>OPENCLAW_DISABLE_BUNDLED_PLUGINS</code> as a hard disable. (#72817) Thanks @serkonyc.</li>
|
||||
<li>Agents/ACPX: stop forwarding Codex ACP timeout config controls that Codex rejects while preserving OpenClaw's run-timeout watchdog for ACP subagents. Fixes #73052. Thanks @pfrederiksen and @richa65.</li>
|
||||
<li>Memory Core: stream fallback vector search scoring with a bounded top-K result set so large indexes do not materialize every chunk embedding when sqlite-vec is unavailable. (#73069) Thanks @parkertoddbrooks.</li>
|
||||
<li>Memory Core: stream embedding-cache seeding during safe reindex so large local caches do not materialize every row into the V8 heap before the atomic rebuild. (#73067) Thanks @parkertoddbrooks.</li>
|
||||
<li>Memory/Ollama: add <code>memorySearch.remote.nonBatchConcurrency</code> for inline embedding indexing, default Ollama non-batch indexing to one request at a time, and keep batch concurrency separate from non-batch concurrency so local embedding backfills avoid timeout storms on smaller hosts. Carries forward #57733. Thanks @itilys.</li>
|
||||
<li>macOS app: update Peekaboo, ElevenLabsKit, and MLX TTS helper dependencies, make canvas file watching and config/exec-approval state writes reliable under concurrent app/test activity, and keep the app plus helper builds warning-free. Thanks @Blaizzy.</li>
|
||||
<li>iOS app: refresh SwiftPM/XcodeGen source hygiene, make app, extension, watch, and curated shared Swift files pass the prebuild SwiftFormat and SwiftLint checks, move relay registration off deprecated StoreKit receipt APIs, and keep simulator builds and logic tests warning-free. Thanks @ngutman.</li>
|
||||
<li>Agents/models: keep <code>models.json</code> readiness and provider-hook caches warm across repeated agent and subagent model resolution while preserving external <code>models.json</code> invalidation, reducing repeated provider-plugin loads on slower ARM64 hosts. Fixes #73075. Thanks @jochen.</li>
|
||||
<li>Docs/tools: clarify that <code>tools.profile: "messaging"</code> is intentionally narrow and that <code>tools.profile: "full"</code> is the unrestricted baseline for broader command/control access. Carries forward #39954. Thanks @posigit.</li>
|
||||
<li>Control UI/Agents: redact tool-call args, partial/final results, derived exec output, and configured custom secret patterns before streaming tool events to the Control UI, so tool output cannot expose provider or channel credentials. Fixes #72283. (#72319) Thanks @volcano303 and @BunsDev.</li>
|
||||
<li>Agents/sessions: keep <code>sessions_history</code> recall redaction enabled even when general log redaction is disabled, and clarify that safety-boundary UI/tool/diagnostic payloads still redact independently of <code>logging.redactSensitive</code>. Carries forward #72319. Thanks @volcano303 and @BunsDev.</li>
|
||||
<li>Providers/Codex: pass agent and workspace directories into provider stream wrappers so Codex native <code>web_search</code> activation can evaluate the correct auth context, and smoke-test the built status-message runtime by resolving the emitted bundle name. Carries forward #67843; refs #65909. Thanks @neilofneils404.</li>
|
||||
<li>Cron/models: keep <code>payload.model</code> as a per-job primary that can use configured fallbacks, while still letting <code>payload.fallbacks: []</code> make cron runs strict and avoid hidden agent-primary retries. Refs #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>Models/fallbacks: treat user-selected session models as exact choices, so <code>/model ollama/...</code> and model-picker switches fail visibly when the selected provider is unreachable instead of answering from an unrelated configured fallback. Fixes #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>Codex harness: keep ChatGPT subscription app-server runs from inheriting <code>CODEX_API_KEY</code> or <code>OPENAI_API_KEY</code>, and fall back to <code>CODEX_API_KEY</code> / <code>OPENAI_API_KEY</code> app-server login only when no Codex account is available. Fixes #73057. Thanks @holgergruenhagen and @pashpashpash.</li>
|
||||
<li>CLI/model probes: fail local <code>infer model run</code> probes when the provider returns no text output, so unreachable local providers and empty completions no longer look like successful smoke tests. Refs #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>CLI/Ollama: run local <code>infer model run</code> through the lean provider completion path and skip global model discovery for one-shot local probes, so Ollama smoke tests no longer pay full chat-agent/tool startup cost or hang before the native <code>/api/chat</code> request. Fixes #72851. Thanks @TotalRes2020.</li>
|
||||
<li>Doctor/gateway services: ignore launchd/systemd companion services that only reference the gateway as a dependency, suppress inactive Linux extra-service warnings, and avoid rewriting a running systemd gateway command/entrypoint during doctor repair. Carries forward #39118. Thanks @therk.</li>
|
||||
<li>Daemon/service: only emit hard-coded version-manager paths such as <code>~/.volta/bin</code>, <code>~/.asdf/shims</code>, <code>~/.bun/bin</code>, and fnm/pnpm fallbacks into gateway and node service PATHs when the directories exist, so <code>openclaw doctor</code> no longer flags <code>gateway.path.non-minimal</code> against a PATH the daemon just wrote. Env-driven roots and stable user-bin dirs remain unconditional. Fixes #71944; carries forward #71964. Thanks @Sanjays2402.</li>
|
||||
<li>CLI/startup: disable Node's module compile cache automatically for live source-checkout launchers so in-place <code>pnpm build</code> updates are visible to the next <code>openclaw</code> CLI invocation. Fixes #73037. Thanks @LouisGameDev.</li>
|
||||
<li>Agents/group chat: keep silent-allowed empty and reasoning-only turns on the <code>NO_REPLY</code> path without injecting visible-answer retry prompts, and clarify the group prompt so agents use the exact silent token instead of prose. Thanks @vincentkoc.</li>
|
||||
<li>Agents/group chat: move <code>NO_REPLY</code> mechanics into channel-aware direct/group prompts and suppress the duplicate generic silent-reply section for auto-reply runs, so always-on group agents get one consistent stay-silent instruction. Thanks @vincentkoc.</li>
|
||||
<li>Providers/OpenAI: preserve encrypted empty-summary Responses reasoning items in WebSocket replay and request <code>reasoning.encrypted_content</code> on reasoning turns so GPT-5.4/GPT-5.5 sessions do not lose required <code>rs_*</code> state beside <code>msg_*</code> items. Fixes #73053. Thanks @odb36777.</li>
|
||||
<li>Gateway/startup: treat <code>plugins.enabled=false</code> as an early plugin fast path, skipping plugin auto-enable discovery, gateway plugin lookup/runtime-dependency staging, and stale-plugin cleanup warnings while preserving channel blocker warnings. (#73041) Thanks @WuKongAI-CMU.</li>
|
||||
<li>Channels/commands: make generated <code>/dock-*</code> commands switch the active session reply route through <code>session.identityLinks</code> instead of falling through to normal chat. Fixes #69206; carries forward #73033. Thanks @clawbones and @michaelatamuk.</li>
|
||||
<li>Providers/Cloudflare AI Gateway: strip assistant prefill turns from Anthropic Messages payloads when thinking is enabled, so Claude requests through Cloudflare AI Gateway no longer fail Anthropic conversation-ending validation. Fixes #72905; carries forward #73005. Thanks @AaronFaby and @sahilsatralkar.</li>
|
||||
<li>Gateway/startup: keep primary-model startup prewarm on scoped metadata preparation, let native approval bootstraps retry outside channel startup, and skip the global hook runner when no <code>gateway_start</code> hook is registered, so clean post-ready sidecar work stays off the critical path. Refs #72846. Thanks @RayWoo, @livekm0309, and @mrz1836.</li>
|
||||
<li>Gateway/channels: start bundled channel accounts with a lightweight <code>runtimeContexts</code> surface instead of importing the full reply/routing/session channel runtime before <code>startAccount</code>, so Discord, Telegram, Slack, Matrix, and QQBot startup no longer block on unrelated channel helper graphs. Refs #72846 and #72960. Thanks @mrz1836, @RayWoo, and @rollingshmily.</li>
|
||||
<li>Gateway/supervisor: exit cleanly when a supervised restart finds an existing healthy gateway and bound retries when the existing gateway stays unhealthy, so stale lock contention cannot loop indefinitely. Refs #72846. Thanks @azgardtek.</li>
|
||||
<li>Gateway/startup: scope primary-model provider discovery during channel prewarm to the configured provider owner and add split startup trace timings, so boot avoids staging unrelated bundled provider dependencies while setup discovery remains broad. Fixes #73002. Thanks @Schnup03.</li>
|
||||
<li>Plugins/runtime deps: declare retained staged bundled plugin dependencies in the npm staging manifest while installing only newly missing packages, so Gateway restarts avoid reinstalling the full retained dependency set when one runtime dependency is absent. Fixes #73055. Thanks @GCorp2026.</li>
|
||||
<li>CLI/status: keep default <code>openclaw status</code> off the heavyweight security audit, plugin compatibility, and memory-vector probes while still showing configured Telegram channels through setup metadata, so routine health checks stay fast and no longer render an empty Channels table. Fixes #72993. Thanks @comick1.</li>
|
||||
<li>Channels/Telegram: send a best-effort native typing cue immediately after an inbound message is accepted, so slow pre-dispatch turns show Telegram liveness before queueing, compaction, model, or tool work starts. Fixes #63759. Thanks @alessandropcostabr.</li>
|
||||
<li>Channels/Telegram: stop native approval startup auth failures from retrying every second, while still waiting through retryable Gateway auth handoffs, so Telegram approval setup problems no longer create a reconnect/log loop during channel startup. Refs #72846 and #72867. Thanks @kiranvk-2011 and @porly1985.</li>
|
||||
<li>Channels/Microsoft Teams: unwrap staged CommonJS JWT runtime dependencies before Bot Connector token validation so inbound Teams messages no longer 401 after the bundled runtime-deps move. Fixes #73026. Thanks @kbrown10000.</li>
|
||||
<li>Gateway/auth: allow local direct callers in trusted-proxy mode to use the configured gateway password as an internal fallback while keeping token fallback rejected. Fixes #17761. Thanks @dashed, @vincentkoc, and @jetd1.</li>
|
||||
<li>Gateway/auth: add explicit <code>trustedProxy.allowLoopback</code> support for same-host loopback reverse proxies while keeping loopback trusted-proxy auth fail-closed by default and preserving required-header and allowlist checks. Fixes #59167; carries forward #63379. Thanks @Matir, @jeremyakers, and @mrosmarin.</li>
|
||||
<li>Channels/sessions: prevent guarded inbound session recording from creating route-only phantom sessions while still allowing last-route updates for sessions that already exist. Carries forward #73009. Thanks @jzakirov.</li>
|
||||
<li>Cron: accept <code>delivery.threadId</code> in Gateway cron add/update schemas so scheduled announce delivery can target Telegram forum topics and other threaded channel destinations through the documented delivery path. Fixes #73017. Thanks @coachsootz.</li>
|
||||
<li>Plugins/runtime deps: stage bundled plugin dependencies imported by mirrored root dist chunks, so packaged memory and status commands do not miss <code>chokidar</code> or similar root-chunk dependencies after update. Fixes #72882 and #72970; carries forward #72992. Thanks @shrimpy8, @colin-chang, and @Schnup03.</li>
|
||||
<li>Plugins/runtime deps: reuse unchanged bundled plugin runtime mirrors instead of rebuilding plugin trees on every load, cutting avoidable writes and restart/reconnect I/O on slow storage. Fixes #72933. Thanks @jasonftl.</li>
|
||||
<li>Agents/runtime context: deliver hidden runtime context through prompt-local system context while keeping the transcript-only custom entry out of provider user turns, and strip stale copied runtime-context prefaces from user-facing replies. Fixes #72386; carries forward #72969. Thanks @jhsmith409.</li>
|
||||
<li>Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg.</li>
|
||||
<li>CLI/message: resolve targeted <code>openclaw message</code> channels to their owning plugin before loading the registry, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans without assuming channel ids match plugin ids. Fixes #73006. Thanks @jasonftl.</li>
|
||||
<li>Plugins/startup: parse strict JSON plugin manifests with native JSON first and keep JSON5 as the compatibility fallback, reducing manifest registry CPU during Gateway boot and CLI startup. Fixes #73011. Thanks @jasonftl.</li>
|
||||
<li>CLI/models: keep route-first <code>models status --json</code> stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar.</li>
|
||||
<li>Gateway/runtime: keep dirty-tree status calls from rebuilding live <code>dist</code>, clear stale task and restart state across in-process restarts, retry transient Discord lazy imports, and let channel startup continue after slow model warmup so browser, Discord, and voice-call sidecars come online. Thanks @vincentkoc.</li>
|
||||
<li>Security/CodeQL: replace file SecretRef id gateway schema regex validation with segment-aligned predicates and set empty permissions on release summary/backfill jobs so the narrowed CodeQL profile stays clean. Thanks @vincentkoc.</li>
|
||||
<li>Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future <code>updatedAt</code> values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon.</li>
|
||||
<li>Sessions: apply search, activity filters, and limits before gateway row enrichment so bounded session lists avoid scanning discarded transcripts. Carries forward #72978. Thanks @yeager.</li>
|
||||
<li>Sessions: remove trajectory runtime and pointer sidecars when session maintenance prunes, caps, or disk-evicts their owning session, while preserving sidecars still referenced by live rows. Fixes #73000. Thanks @jared-rebel.</li>
|
||||
<li>Plugins/CLI: allow managed plugin installs when the active extensions root is a symlink to a real state directory, while keeping nested target symlinks blocked and suppressing misleading hook-pack fallback errors for install-boundary failures. Fixes #72946. Thanks @mayank6136.</li>
|
||||
<li>Providers/Ollama: mark discovered Ollama catalog models as supporting streaming usage metadata so token accounting stays enabled for local models. (#72976) Thanks @sdeyang.</li>
|
||||
<li>Media understanding: reject malformed MIME values with trailing junk while preserving standard parameter tails before enrichment uses them. (#72914) Thanks @volcano303.</li>
|
||||
<li>WebChat: keep bare <code>/new</code> and <code>/reset</code> prompts from producing empty transcript text by inserting the hidden session marker when the visible tail is blank. (#72863) Thanks @mahopan.</li>
|
||||
<li>CLI/update: explain completion-cache refresh timeouts with manual refresh guidance instead of surfacing a raw low-level timeout. Fixes #72842. (#72850) Thanks @iot2edge.</li>
|
||||
<li>Memory-core/dreaming: give narrative generation a 60-second timeout so slower local or remote models can finish instead of timing out at 15 seconds. Fixes #72837. (#72852) Thanks @RayWoo.</li>
|
||||
<li>Plugins/hooks: inject each plugin's resolved config into internal hook event context without mutating the shared event object. (#72888) Thanks @jalapeno777.</li>
|
||||
<li>Agents/ACP: pass the resolved ACP agent directory into media understanding so per-agent media caches and config are used for ACP-dispatched image turns. (#72832) Thanks @luyao618.</li>
|
||||
<li>Gateway/Bonjour: truncate mDNS service names and host labels to the 63-byte DNS label limit at valid UTF-8 boundaries. (#72809) Thanks @luyao618.</li>
|
||||
<li>Feishu: treat groups explicitly configured under channels.feishu.groups as admitted even when groupAllowFrom is empty, while preserving groupPolicy: "disabled" as a hard group block and keeping groups.\* wildcard defaults non-admitting. Fixes #67687. (#72789) Thanks @MoerAI.</li>
|
||||
<li>Gateway/startup: keep hot Gateway boot paths on leaf config imports and add max-RSS reporting to the gateway startup bench so low-memory startup regressions are visible before release. Thanks @vincentkoc.</li>
|
||||
<li>WebChat: read <code>chat.history</code> from active transcript branches, drop stale streamed assistant tails once final history catches up, and coalesce duplicate in-flight Control UI submits, so rewritten prompts, completed replies, and rapid send events no longer render or process twice. Fixes #72975, #72963, and #72974. Thanks @dmagdici, @lhtpluto, and @Benjamin5281999.</li>
|
||||
<li>WebChat/TTS: persist automatic final-mode TTS audio as a supplemental audio-only transcript update instead of adding a second assistant message with the same visible text. Fixes #72830. Thanks @lhtpluto.</li>
|
||||
<li>Agents/LSP: terminate bundled stdio LSP process trees during runtime disposal and Gateway shutdown, so nested children such as <code>tsserver</code> do not survive stop or restart. Fixes #72357. Thanks @ai-hpc and @bittoby.</li>
|
||||
<li>Diagnostics/OTEL: capture privacy-safe model-call request payload bytes, streamed response bytes, first-response latency, and total duration in diagnostic events, plugin hooks, stability snapshots, and OTEL model-call spans/metrics without logging raw model content. Fixes #33832. Thanks @wwh830.</li>
|
||||
<li>Logging: write validated diagnostic trace context as top-level <code>traceId</code>, <code>spanId</code>, <code>parentSpanId</code>, and <code>traceFlags</code> fields in file-log JSONL records so traced requests and model calls are easier to correlate in log processors. Refs #40353. Thanks @liangruochong44-ui.</li>
|
||||
<li>Logging/sessions: apply configured redaction patterns to persisted session transcript text and accept escaped character classes in safe custom redaction regexes, so transcript JSONL no longer keeps matching sensitive text in the clear. Fixes #42982. Thanks @panpan0000.</li>
|
||||
<li>Providers/Ollama: honor <code>/api/show</code> capabilities when registering local models so non-tool Ollama models no longer receive the agent tool surface, and keep native Ollama thinking opt-in instead of enabling it by default. Fixes #64710 and duplicate #65343. Thanks @yuan-b, @netherby, @xilopaint, and @Diyforfun2026.</li>
|
||||
<li>Control UI/Agents: remount the Overview model controls when switching agents so the primary-model picker cannot retain stale per-agent selection. Fixes #39392; carries forward #39401, notes the duplicate #39495 approach, and keeps #46275/#54724 broader stabilization out of scope. Thanks @daijunyi002, @SergioChan, @aworki, and @wsyjh8.</li>
|
||||
<li>Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.</li>
|
||||
<li>Agents/model fallback: jump directly to a known later live-session model redirect instead of walking unrelated fallback candidates, while preserving the already-landed live-session/fallback loop guard. Fixes #57471; related loop family already closed via #58496. Thanks @yuxiaoyang2007-prog.</li>
|
||||
<li>Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/startup: load the default <code>memory-core</code> slot during Gateway startup when permitted so active-memory recall can call <code>memory_search</code> and <code>memory_get</code> without requiring an explicit <code>plugins.slots.memory</code> entry, while preserving <code>plugins.slots.memory: "none"</code>. Thanks @vincentkoc.</li>
|
||||
<li>Gateway/plugins: resolve <code>gateway_start</code> cron hooks from live Gateway runtime state before the legacy deps fallback, so memory-core dreaming cron reconciliation keeps working on installs where <code>deps.cron</code> is not populated during service startup. Fixes #72835. Thanks @RayWoo.</li>
|
||||
<li>Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.</li>
|
||||
<li>Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale <code>plugins list</code> entries. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @vincentkoc.</li>
|
||||
<li>Plugins: fail <code>plugins update</code> when tracked plugin or hook updates error, keep bundled runtime-dependency repair behind restrictive allowlists, and reject package installs with unloadable extension entries. Thanks @vincentkoc.</li>
|
||||
<li>WebChat/Control UI: support non-video file attachments in chat uploads while preserving the existing image attachment path and MIME-sniff fallback for generic image uploads. (#70947) Thanks @IAMSamuelRodda.</li>
|
||||
<li>Skills/memory: restore Chokidar v5 hot reloads by watching concrete skill and memory roots with filters, including SKILL.md removals and deleted skill folders without broad workspace recursion. Fixes #27404, #33585, and #41606. Thanks @shelvenzhou, @08820048, and @rocke2020.</li>
|
||||
<li>Gateway/chat: keep duplicate attachment-backed <code>chat.send</code> retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.</li>
|
||||
<li>Gateway/chat: preserve repeated boundary characters while merging assistant chat stream deltas, including repeated digits, CJK characters, and markdown/table tokens. Fixes #63769; carries forward #63994 and #65457. Thanks @yon950905 and @mohuaxiao.</li>
|
||||
<li>Plugins: share package entrypoint resolution between install and discovery, reject mismatched <code>runtimeExtensions</code>, and cache bundled runtime-dependency manifest reads during scans. Thanks @vincentkoc.</li>
|
||||
<li>WhatsApp/Web: keep quiet but healthy linked-device sessions connected by basing the watchdog on WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Fixes #70678; carries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.</li>
|
||||
<li>Discord/gateway: count failed health-monitor restart attempts toward cooldown and hourly caps, and evict stale account lifecycle state during channel reloads so repeated Discord gateway recovery cannot loop on old status. Fixes #38596. (#40413) Thanks @jellyAI-dev and @vashquez.</li>
|
||||
<li>TTS/BlueBubbles: pre-transcode synthesized MP3 audio to opus-in-CAF (mono, 24 kHz — validated against macOS 15.x Messages.app's native voice-memo CAF descriptor) on macOS hosts before handing the file to BlueBubbles, so iMessage renders the result as a native voice-memo bubble with proper duration and waveform UI instead of a plain file attachment. Adds an opt-in <code>tts.voice.preferAudioFileFormat</code> channel capability and a magic-byte sniff for the CAF container so the host-local-media validator (which uses <code>file-type</code> and didn't recognize CAF natively) can verify the pre-transcoded buffer. Channels that don't opt in are unaffected. (#72586) Fixes #72506. Thanks @omarshahine.</li>
|
||||
<li>Feishu: retry WebSocket startup failures with monitor-owned backoff while preserving SDK-local heartbeat defaults, so persistent-connection startup failures no longer leave the monitor hung. Fixes #68766; related #42354 and #55532. Thanks @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.27/OpenClaw-2026.4.27.zip" length="50595360" type="application/octet-stream" sparkle:edSignature="X8DQNQNWVcvtpYLkhZcsKNpnA78ycyzgGlZaG0XBY1GIph3oZNUIpAszGGocJVqTK7+F89Au5ZPb60mOqJQ6DQ=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -285,7 +285,7 @@ Common failure quick-fixes:
|
||||
- `pairing required` before tests start:
|
||||
- approve pending device pairing (`openclaw devices approve --latest`) and rerun.
|
||||
- `A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`:
|
||||
- ensure gateway canvas host is running and reachable, keep the app on the **Screen** tab. The app will auto-refresh canvas capability once; if it still fails, reconnect app and rerun.
|
||||
- ensure the Canvas plugin host is running and reachable, keep the app on the **Screen** tab. The app refreshes the Canvas plugin surface URL once before failing; if it still fails, reconnect app and rerun.
|
||||
- `NODE_BACKGROUND_UNAVAILABLE: canvas unavailable`:
|
||||
- app is not effectively ready for canvas commands; keep app foregrounded and **Screen** tab active.
|
||||
|
||||
|
||||
@@ -233,13 +233,13 @@ class NodeRuntime(
|
||||
smsTelephonyAvailable = { sms.hasTelephonyFeature() },
|
||||
callLogAvailable = { SensitiveFeatureConfig.callLogEnabled },
|
||||
debugBuild = { BuildConfig.DEBUG },
|
||||
refreshNodeCanvasCapability = { nodeSession.refreshNodeCanvasCapability() },
|
||||
onCanvasA2uiPush = {
|
||||
_canvasA2uiHydrated.value = true
|
||||
_canvasRehydratePending.value = false
|
||||
_canvasRehydrateErrorText.value = null
|
||||
},
|
||||
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
|
||||
refreshCanvasHostUrl = { nodeSession.refreshCanvasHostUrl() },
|
||||
motionActivityAvailable = { motionHandler.isActivityAvailable() },
|
||||
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
|
||||
)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package ai.openclaw.app.gateway
|
||||
|
||||
const val GATEWAY_PROTOCOL_VERSION = 3
|
||||
const val GATEWAY_PROTOCOL_VERSION = 4
|
||||
|
||||
@@ -135,7 +135,7 @@ class GatewaySession(
|
||||
private val writeLock = Mutex()
|
||||
private val pending = ConcurrentHashMap<String, CompletableDeferred<RpcResponse>>()
|
||||
|
||||
@Volatile private var canvasHostUrl: String? = null
|
||||
@Volatile private var pluginSurfaceUrls: Map<String, String> = emptyMap()
|
||||
|
||||
@Volatile private var mainSessionKey: String? = null
|
||||
|
||||
@@ -185,7 +185,7 @@ class GatewaySession(
|
||||
scope.launch(Dispatchers.IO) {
|
||||
job?.cancelAndJoin()
|
||||
job = null
|
||||
canvasHostUrl = null
|
||||
pluginSurfaceUrls = emptyMap()
|
||||
mainSessionKey = null
|
||||
onDisconnected("Offline")
|
||||
}
|
||||
@@ -196,7 +196,20 @@ class GatewaySession(
|
||||
currentConnection?.closeQuietly()
|
||||
}
|
||||
|
||||
fun currentCanvasHostUrl(): String? = canvasHostUrl
|
||||
fun currentCanvasHostUrl(): String? = pluginSurfaceUrls["canvas"]
|
||||
|
||||
suspend fun refreshCanvasHostUrl(timeoutMs: Long = 8_000): String? {
|
||||
val refreshed =
|
||||
refreshPluginSurfaceUrl(
|
||||
method = "node.pluginSurface.refresh",
|
||||
params = buildJsonObject { put("surface", JsonPrimitive("canvas")) },
|
||||
timeoutMs = timeoutMs,
|
||||
)
|
||||
if (!refreshed.isNullOrBlank()) {
|
||||
pluginSurfaceUrls = pluginSurfaceUrls + ("canvas" to refreshed)
|
||||
}
|
||||
return refreshed
|
||||
}
|
||||
|
||||
fun currentMainSessionKey(): String? = mainSessionKey
|
||||
|
||||
@@ -218,6 +231,28 @@ class GatewaySession(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshPluginSurfaceUrl(
|
||||
method: String,
|
||||
params: JsonElement?,
|
||||
timeoutMs: Long,
|
||||
): String? {
|
||||
val conn = currentConnection ?: return null
|
||||
return try {
|
||||
val res = conn.request(method, params, timeoutMs)
|
||||
if (!res.ok) return null
|
||||
val obj = res.payloadJson?.let { json.parseToJsonElement(it).asObjectOrNull() } ?: return null
|
||||
val raw =
|
||||
obj["pluginSurfaceUrls"]
|
||||
.asObjectOrNull()
|
||||
?.get("canvas")
|
||||
.asStringOrNull()
|
||||
normalizeCanvasHostUrl(raw, conn.endpoint, isTlsConnection = conn.tls != null)
|
||||
} catch (err: Throwable) {
|
||||
Log.d("OpenClawGateway", "$method failed: ${err.message ?: err::class.java.simpleName}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendNodeEventDetailed(
|
||||
event: String,
|
||||
payloadJson: String?,
|
||||
@@ -280,52 +315,6 @@ class GatewaySession(
|
||||
return RpcResult(ok = res.ok, payloadJson = res.payloadJson, error = res.error)
|
||||
}
|
||||
|
||||
suspend fun refreshNodeCanvasCapability(timeoutMs: Long = 8_000): Boolean {
|
||||
val conn = currentConnection ?: return false
|
||||
val response =
|
||||
try {
|
||||
conn.request(
|
||||
"node.canvas.capability.refresh",
|
||||
params = buildJsonObject {},
|
||||
timeoutMs = timeoutMs,
|
||||
)
|
||||
} catch (err: Throwable) {
|
||||
Log.w("OpenClawGateway", "node.canvas.capability.refresh failed: ${err.message ?: err::class.java.simpleName}")
|
||||
return false
|
||||
}
|
||||
if (!response.ok) {
|
||||
val err = response.error
|
||||
Log.w(
|
||||
"OpenClawGateway",
|
||||
"node.canvas.capability.refresh rejected: ${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}",
|
||||
)
|
||||
return false
|
||||
}
|
||||
val payloadObj = response.payloadJson?.let(::parseJsonOrNull)?.asObjectOrNull()
|
||||
val refreshedCapability =
|
||||
payloadObj
|
||||
?.get("canvasCapability")
|
||||
.asStringOrNull()
|
||||
?.trim()
|
||||
.orEmpty()
|
||||
if (refreshedCapability.isEmpty()) {
|
||||
Log.w("OpenClawGateway", "node.canvas.capability.refresh missing canvasCapability")
|
||||
return false
|
||||
}
|
||||
val scopedCanvasHostUrl = canvasHostUrl?.trim().orEmpty()
|
||||
if (scopedCanvasHostUrl.isEmpty()) {
|
||||
Log.w("OpenClawGateway", "node.canvas.capability.refresh missing local canvasHostUrl")
|
||||
return false
|
||||
}
|
||||
val refreshedUrl = replaceCanvasCapabilityInScopedHostUrl(scopedCanvasHostUrl, refreshedCapability)
|
||||
if (refreshedUrl == null) {
|
||||
Log.w("OpenClawGateway", "node.canvas.capability.refresh unable to rewrite scoped canvas URL")
|
||||
return false
|
||||
}
|
||||
canvasHostUrl = refreshedUrl
|
||||
return true
|
||||
}
|
||||
|
||||
private data class RpcResponse(
|
||||
val id: String,
|
||||
val ok: Boolean,
|
||||
@@ -334,12 +323,12 @@ class GatewaySession(
|
||||
)
|
||||
|
||||
private inner class Connection(
|
||||
private val endpoint: GatewayEndpoint,
|
||||
val endpoint: GatewayEndpoint,
|
||||
private val token: String?,
|
||||
private val bootstrapToken: String?,
|
||||
private val password: String?,
|
||||
private val options: GatewayConnectOptions,
|
||||
private val tls: GatewayTlsParams?,
|
||||
val tls: GatewayTlsParams?,
|
||||
) {
|
||||
private val connectDeferred = CompletableDeferred<Unit>()
|
||||
private val closedDeferred = CompletableDeferred<Unit>()
|
||||
@@ -615,8 +604,13 @@ class GatewaySession(
|
||||
}
|
||||
}
|
||||
}
|
||||
val rawCanvas = obj["canvasHostUrl"].asStringOrNull()
|
||||
canvasHostUrl = normalizeCanvasHostUrl(rawCanvas, endpoint, isTlsConnection = tls != null)
|
||||
val rawPluginSurfaceUrls = obj["pluginSurfaceUrls"].asObjectOrNull()
|
||||
val normalizedPluginSurfaceUrls =
|
||||
rawPluginSurfaceUrls?.mapNotNull { (surface, value) ->
|
||||
normalizeCanvasHostUrl(value.asStringOrNull(), endpoint, isTlsConnection = tls != null)
|
||||
?.let { normalized -> surface to normalized }
|
||||
} ?: emptyList()
|
||||
pluginSurfaceUrls = normalizedPluginSurfaceUrls.toMap()
|
||||
val sessionDefaults =
|
||||
obj["snapshot"]
|
||||
.asObjectOrNull()
|
||||
@@ -910,7 +904,7 @@ class GatewaySession(
|
||||
conn.awaitClose()
|
||||
} finally {
|
||||
currentConnection = null
|
||||
canvasHostUrl = null
|
||||
pluginSurfaceUrls = emptyMap()
|
||||
mainSessionKey = null
|
||||
}
|
||||
}
|
||||
@@ -1133,22 +1127,6 @@ private fun parseJsonOrNull(payload: String): JsonElement? {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun replaceCanvasCapabilityInScopedHostUrl(
|
||||
scopedUrl: String,
|
||||
capability: String,
|
||||
): String? {
|
||||
val marker = "/__openclaw__/cap/"
|
||||
val markerStart = scopedUrl.indexOf(marker)
|
||||
if (markerStart < 0) return null
|
||||
val capabilityStart = markerStart + marker.length
|
||||
val slashEnd = scopedUrl.indexOf("/", capabilityStart).takeIf { it >= 0 }
|
||||
val queryEnd = scopedUrl.indexOf("?", capabilityStart).takeIf { it >= 0 }
|
||||
val fragmentEnd = scopedUrl.indexOf("#", capabilityStart).takeIf { it >= 0 }
|
||||
val capabilityEnd = listOfNotNull(slashEnd, queryEnd, fragmentEnd).minOrNull() ?: scopedUrl.length
|
||||
if (capabilityEnd <= capabilityStart) return null
|
||||
return scopedUrl.substring(0, capabilityStart) + capability + scopedUrl.substring(capabilityEnd)
|
||||
}
|
||||
|
||||
internal fun resolveInvokeResultAckTimeoutMs(invokeTimeoutMs: Long?): Long {
|
||||
val normalized = invokeTimeoutMs?.takeIf { it > 0L } ?: 15_000L
|
||||
return normalized.coerceIn(15_000L, 120_000L)
|
||||
|
||||
@@ -78,9 +78,9 @@ class InvokeDispatcher(
|
||||
private val smsTelephonyAvailable: () -> Boolean,
|
||||
private val callLogAvailable: () -> Boolean,
|
||||
private val debugBuild: () -> Boolean,
|
||||
private val refreshNodeCanvasCapability: suspend () -> Boolean,
|
||||
private val onCanvasA2uiPush: () -> Unit,
|
||||
private val onCanvasA2uiReset: () -> Unit,
|
||||
private val refreshCanvasHostUrl: suspend () -> String?,
|
||||
private val motionActivityAvailable: () -> Boolean,
|
||||
private val motionPedometerAvailable: () -> Boolean,
|
||||
) {
|
||||
@@ -231,23 +231,15 @@ class InvokeDispatcher(
|
||||
private suspend fun withReadyA2ui(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult {
|
||||
var a2uiUrl =
|
||||
a2uiHandler.resolveA2uiHostUrl()
|
||||
?: refreshCanvasHostUrl().let { a2uiHandler.resolveA2uiHostUrl() }
|
||||
?: return GatewaySession.InvokeResult.error(
|
||||
code = "A2UI_HOST_NOT_CONFIGURED",
|
||||
message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
|
||||
)
|
||||
val readyOnFirstCheck = a2uiHandler.ensureA2uiReady(a2uiUrl)
|
||||
if (!readyOnFirstCheck) {
|
||||
if (!refreshNodeCanvasCapability()) {
|
||||
return GatewaySession.InvokeResult.error(
|
||||
code = "A2UI_HOST_UNAVAILABLE",
|
||||
message = "A2UI_HOST_UNAVAILABLE: A2UI host not reachable",
|
||||
)
|
||||
}
|
||||
a2uiUrl = a2uiHandler.resolveA2uiHostUrl()
|
||||
?: return GatewaySession.InvokeResult.error(
|
||||
code = "A2UI_HOST_NOT_CONFIGURED",
|
||||
message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
|
||||
)
|
||||
refreshCanvasHostUrl()
|
||||
a2uiUrl = a2uiHandler.resolveA2uiHostUrl() ?: a2uiUrl
|
||||
if (!a2uiHandler.ensureA2uiReady(a2uiUrl)) {
|
||||
return GatewaySession.InvokeResult.error(
|
||||
code = "A2UI_HOST_UNAVAILABLE",
|
||||
|
||||
@@ -476,56 +476,6 @@ class GatewaySessionInvokeTest {
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun refreshNodeCanvasCapability_sendsObjectParamsAndUpdatesScopedUrl() =
|
||||
runBlocking {
|
||||
val json = testJson()
|
||||
val connected = CompletableDeferred<Unit>()
|
||||
val refreshRequestParams = CompletableDeferred<String?>()
|
||||
val lastDisconnect = AtomicReference("")
|
||||
|
||||
val server =
|
||||
startGatewayServer(json) { webSocket, id, method, frame ->
|
||||
when (method) {
|
||||
"connect" -> {
|
||||
webSocket.send(connectResponseFrame(id, canvasHostUrl = "http://127.0.0.1/__openclaw__/cap/old-cap"))
|
||||
}
|
||||
"node.canvas.capability.refresh" -> {
|
||||
if (!refreshRequestParams.isCompleted) {
|
||||
refreshRequestParams.complete(frame["params"]?.toString())
|
||||
}
|
||||
webSocket.send(
|
||||
"""{"type":"res","id":"$id","ok":true,"payload":{"canvasCapability":"new-cap"}}""",
|
||||
)
|
||||
webSocket.close(1000, "done")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val harness =
|
||||
createNodeHarness(
|
||||
connected = connected,
|
||||
lastDisconnect = lastDisconnect,
|
||||
) { GatewaySession.InvokeResult.ok("""{"handled":true}""") }
|
||||
|
||||
try {
|
||||
connectNodeSession(harness.session, server.port)
|
||||
awaitConnectedOrThrow(connected, lastDisconnect, server)
|
||||
|
||||
val refreshed = harness.session.refreshNodeCanvasCapability(timeoutMs = TEST_TIMEOUT_MS)
|
||||
val refreshParamsJson = withTimeout(TEST_TIMEOUT_MS) { refreshRequestParams.await() }
|
||||
|
||||
assertEquals(true, refreshed)
|
||||
assertEquals("{}", refreshParamsJson)
|
||||
assertEquals(
|
||||
"http://127.0.0.1:${server.port}/__openclaw__/cap/new-cap",
|
||||
harness.session.currentCanvasHostUrl(),
|
||||
)
|
||||
} finally {
|
||||
shutdownHarness(harness, server)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sendNodeEventDetailed_sendsPresenceAlivePayloadAndReturnsStructuredResponse() =
|
||||
runBlocking {
|
||||
@@ -778,12 +728,17 @@ class GatewaySessionInvokeTest {
|
||||
|
||||
private fun connectResponseFrame(
|
||||
id: String,
|
||||
canvasHostUrl: String? = null,
|
||||
pluginSurfaceUrls: Map<String, String> = emptyMap(),
|
||||
authJson: String? = null,
|
||||
): String {
|
||||
val canvas = canvasHostUrl?.let { "\"canvasHostUrl\":\"$it\"," } ?: ""
|
||||
val surfaces =
|
||||
pluginSurfaceUrls.entries
|
||||
.joinToString(",") { (key, value) -> """"$key":"$value"""" }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.let { """"pluginSurfaceUrls":{$it},""" }
|
||||
?: ""
|
||||
val auth = authJson?.let { "\"auth\":$it," } ?: ""
|
||||
return """{"type":"res","id":"$id","ok":true,"payload":{$canvas$auth"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}"""
|
||||
return """{"type":"res","id":"$id","ok":true,"payload":{$surfaces$auth"snapshot":{"sessionDefaults":{"mainSessionKey":"main"}}}}"""
|
||||
}
|
||||
|
||||
private fun startGatewayServer(
|
||||
|
||||
@@ -39,26 +39,4 @@ class GatewaySessionInvokeTimeoutTest {
|
||||
assertEquals(120_000L, resolveInvokeResultAckTimeoutMs(121_000L))
|
||||
assertEquals(120_000L, resolveInvokeResultAckTimeoutMs(Long.MAX_VALUE))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replaceCanvasCapabilityInScopedHostUrl_rewritesTerminalCapabilitySegment() {
|
||||
assertEquals(
|
||||
"http://127.0.0.1:18789/__openclaw__/cap/new-token",
|
||||
replaceCanvasCapabilityInScopedHostUrl(
|
||||
"http://127.0.0.1:18789/__openclaw__/cap/old-token",
|
||||
"new-token",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replaceCanvasCapabilityInScopedHostUrl_rewritesWhenQueryAndFragmentPresent() {
|
||||
assertEquals(
|
||||
"http://127.0.0.1:18789/__openclaw__/cap/new-token?a=1#frag",
|
||||
replaceCanvasCapabilityInScopedHostUrl(
|
||||
"http://127.0.0.1:18789/__openclaw__/cap/old-token?a=1#frag",
|
||||
"new-token",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,9 +286,9 @@ class InvokeDispatcherTest {
|
||||
smsTelephonyAvailable = { smsTelephonyAvailable },
|
||||
callLogAvailable = { callLogAvailable },
|
||||
debugBuild = { debugBuild },
|
||||
refreshNodeCanvasCapability = { false },
|
||||
onCanvasA2uiPush = {},
|
||||
onCanvasA2uiReset = {},
|
||||
refreshCanvasHostUrl = { null },
|
||||
motionActivityAvailable = { motionActivityAvailable },
|
||||
motionPedometerAvailable = { motionPedometerAvailable },
|
||||
)
|
||||
|
||||
@@ -63,10 +63,9 @@ extension NodeAppModel {
|
||||
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
|
||||
return .ready(initialUrl)
|
||||
}
|
||||
|
||||
// First render can fail when scoped capability rotates between reconnects.
|
||||
guard await self.gatewaySession.refreshNodeCanvasCapability() else { return .hostUnavailable }
|
||||
guard let refreshedUrl = await self.resolveA2UIHostURL() else { return .hostUnavailable }
|
||||
guard let refreshedUrl = await self.resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: true) else {
|
||||
return .hostUnavailable
|
||||
}
|
||||
self.screen.navigate(to: refreshedUrl, trustA2UIActions: true)
|
||||
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
|
||||
return .ready(refreshedUrl)
|
||||
@@ -79,19 +78,19 @@ extension NodeAppModel {
|
||||
self.screen.showDefaultCanvas()
|
||||
}
|
||||
|
||||
private func resolveA2UIHostURLWithCapabilityRefresh() async -> String? {
|
||||
if let url = await self.resolveA2UIHostURL() {
|
||||
return url
|
||||
private func resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
|
||||
if !forceRefresh, let current = await self.resolveA2UIHostURL() {
|
||||
return current
|
||||
}
|
||||
guard await self.gatewaySession.refreshNodeCanvasCapability() else { return nil }
|
||||
_ = await self.gatewaySession.refreshCanvasHostUrl()
|
||||
return await self.resolveA2UIHostURL()
|
||||
}
|
||||
|
||||
private func resolveCanvasHostURLWithCapabilityRefresh() async -> String? {
|
||||
if let url = await self.resolveCanvasHostURL() {
|
||||
return url
|
||||
private func resolveCanvasHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
|
||||
if !forceRefresh, let current = await self.resolveCanvasHostURL() {
|
||||
return current
|
||||
}
|
||||
guard await self.gatewaySession.refreshNodeCanvasCapability() else { return nil }
|
||||
_ = await self.gatewaySession.refreshCanvasHostUrl()
|
||||
return await self.resolveCanvasHostURL()
|
||||
}
|
||||
|
||||
|
||||
@@ -152,15 +152,17 @@ final class CanvasManager {
|
||||
|
||||
private func handleGatewayPush(_ push: GatewayPush) {
|
||||
guard case let .snapshot(snapshot) = push else { return }
|
||||
let raw = snapshot.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
let raw =
|
||||
(snapshot.pluginsurfaceurls?["canvas"]?.value as? String)?
|
||||
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? ""
|
||||
if raw.isEmpty {
|
||||
Self.logger.debug("canvas host url missing in gateway snapshot")
|
||||
Self.logger.debug("canvas plugin surface URL missing in gateway snapshot")
|
||||
} else {
|
||||
Self.logger.debug("canvas host url snapshot=\(raw, privacy: .public)")
|
||||
Self.logger.debug("canvas plugin surface URL snapshot=\(raw, privacy: .public)")
|
||||
}
|
||||
let a2uiUrl = Self.resolveA2UIHostUrl(from: raw)
|
||||
if a2uiUrl == nil, !raw.isEmpty {
|
||||
Self.logger.debug("canvas host url invalid; cannot resolve A2UI")
|
||||
Self.logger.debug("canvas plugin surface URL invalid; cannot resolve A2UI")
|
||||
}
|
||||
guard let controller = self.panelController else {
|
||||
if a2uiUrl != nil {
|
||||
@@ -197,7 +199,7 @@ final class CanvasManager {
|
||||
}
|
||||
|
||||
private func resolveA2UIHostUrl() async -> String? {
|
||||
let raw = await GatewayConnection.shared.canvasHostUrl()
|
||||
let raw = await GatewayConnection.shared.canvasPluginSurfaceUrl()
|
||||
return Self.resolveA2UIHostUrl(from: raw)
|
||||
}
|
||||
|
||||
|
||||
@@ -311,9 +311,10 @@ actor GatewayConnection {
|
||||
self.lastSnapshot = nil
|
||||
}
|
||||
|
||||
func canvasHostUrl() async -> String? {
|
||||
func canvasPluginSurfaceUrl() async -> String? {
|
||||
guard let snapshot = self.lastSnapshot else { return nil }
|
||||
let trimmed = snapshot.canvashosturl?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? ""
|
||||
let raw = snapshot.pluginsurfaceurls?["canvas"]?.value as? String
|
||||
let trimmed = raw?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? ""
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,18 @@ final class MacNodeModeCoordinator {
|
||||
|
||||
private let logger = Logger(subsystem: "ai.openclaw", category: "mac-node")
|
||||
private var task: Task<Void, Never>?
|
||||
private let runtime = MacNodeRuntime()
|
||||
private let session = GatewayNodeSession()
|
||||
private let runtime: MacNodeRuntime
|
||||
private let session: GatewayNodeSession
|
||||
private var autoRepairedTLSFingerprintsByStoreKey: [String: String] = [:]
|
||||
|
||||
private init() {
|
||||
let session = GatewayNodeSession()
|
||||
self.session = session
|
||||
self.runtime = MacNodeRuntime(
|
||||
canvasSurfaceUrl: { await session.currentCanvasHostUrl() },
|
||||
refreshCanvasSurfaceUrl: { await session.refreshCanvasHostUrl() })
|
||||
}
|
||||
|
||||
func start() {
|
||||
guard self.task == nil else { return }
|
||||
self.task = Task { [weak self] in
|
||||
|
||||
@@ -7,6 +7,8 @@ actor MacNodeRuntime {
|
||||
private let cameraCapture = CameraCaptureService()
|
||||
private let makeMainActorServices: () async -> any MacNodeRuntimeMainActorServices
|
||||
private let browserProxyRequest: @Sendable (String?) async throws -> String
|
||||
private let canvasSurfaceUrl: @Sendable () async -> String?
|
||||
private let refreshCanvasSurfaceUrl: @Sendable () async -> String?
|
||||
private var cachedMainActorServices: (any MacNodeRuntimeMainActorServices)?
|
||||
private var mainSessionKey: String = "main"
|
||||
private var eventSender: (@Sendable (String, String?) async -> Void)?
|
||||
@@ -17,10 +19,16 @@ actor MacNodeRuntime {
|
||||
},
|
||||
browserProxyRequest: @escaping @Sendable (String?) async throws -> String = { paramsJSON in
|
||||
try await MacNodeBrowserProxy.shared.request(paramsJSON: paramsJSON)
|
||||
})
|
||||
},
|
||||
canvasSurfaceUrl: @escaping @Sendable () async -> String? = {
|
||||
await GatewayConnection.shared.canvasPluginSurfaceUrl()
|
||||
},
|
||||
refreshCanvasSurfaceUrl: @escaping @Sendable () async -> String? = { nil })
|
||||
{
|
||||
self.makeMainActorServices = makeMainActorServices
|
||||
self.browserProxyRequest = browserProxyRequest
|
||||
self.canvasSurfaceUrl = canvasSurfaceUrl
|
||||
self.refreshCanvasSurfaceUrl = refreshCanvasSurfaceUrl
|
||||
}
|
||||
|
||||
func updateMainSessionKey(_ sessionKey: String) {
|
||||
@@ -441,7 +449,7 @@ actor MacNodeRuntime {
|
||||
|
||||
private func ensureA2UIHost() async throws {
|
||||
if await self.isA2UIReady() { return }
|
||||
guard let a2uiUrl = await self.resolveA2UIHostUrl() else {
|
||||
guard let a2uiUrl = await self.resolveA2UIHostUrlWithCapabilityRefresh() else {
|
||||
throw NSError(domain: "Canvas", code: 30, userInfo: [
|
||||
NSLocalizedDescriptionKey: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
|
||||
])
|
||||
@@ -451,18 +459,35 @@ actor MacNodeRuntime {
|
||||
try CanvasManager.shared.show(sessionKey: sessionKey, path: a2uiUrl)
|
||||
}
|
||||
if await self.isA2UIReady(poll: true) { return }
|
||||
if let refreshedUrl = await self.resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: true) {
|
||||
_ = try await MainActor.run {
|
||||
try CanvasManager.shared.show(sessionKey: sessionKey, path: refreshedUrl)
|
||||
}
|
||||
if await self.isA2UIReady(poll: true) { return }
|
||||
}
|
||||
throw NSError(domain: "Canvas", code: 31, userInfo: [
|
||||
NSLocalizedDescriptionKey: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable",
|
||||
])
|
||||
}
|
||||
|
||||
private func resolveA2UIHostUrl() async -> String? {
|
||||
guard let raw = await GatewayConnection.shared.canvasHostUrl() else { return nil }
|
||||
Self.resolveA2UIHostUrl(from: await self.canvasSurfaceUrl())
|
||||
}
|
||||
|
||||
private static func resolveA2UIHostUrl(from raw: String?) -> String? {
|
||||
guard let raw else { return nil }
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty, let baseUrl = URL(string: trimmed) else { return nil }
|
||||
return baseUrl.appendingPathComponent("__openclaw__/a2ui/").absoluteString + "?platform=macos"
|
||||
}
|
||||
|
||||
func resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
|
||||
if !forceRefresh, let current = await self.resolveA2UIHostUrl() {
|
||||
return current
|
||||
}
|
||||
return Self.resolveA2UIHostUrl(from: await self.refreshCanvasSurfaceUrl())
|
||||
}
|
||||
|
||||
private func isA2UIReady(poll: Bool = false) async -> Bool {
|
||||
let deadline = poll ? Date().addingTimeInterval(6.0) : Date()
|
||||
while true {
|
||||
|
||||
@@ -63,8 +63,12 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
||||
let mainSessionKey = await GatewayConnection.shared.cachedMainSessionKey()
|
||||
let defaults = decoded.defaults.map {
|
||||
OpenClawChatSessionsDefaults(
|
||||
modelProvider: $0.modelProvider,
|
||||
model: $0.model,
|
||||
contextTokens: $0.contextTokens,
|
||||
thinkingLevels: $0.thinkingLevels,
|
||||
thinkingOptions: $0.thinkingOptions,
|
||||
thinkingDefault: $0.thinkingDefault,
|
||||
mainSessionKey: mainSessionKey)
|
||||
} ?? OpenClawChatSessionsDefaults(
|
||||
model: nil,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ struct MacGatewayChatTransportMappingTests {
|
||||
server: [:],
|
||||
features: [:],
|
||||
snapshot: snapshot,
|
||||
canvashosturl: nil,
|
||||
pluginsurfaceurls: nil,
|
||||
auth: [:],
|
||||
policy: [:])
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@ import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct MacNodeRuntimeTests {
|
||||
actor CanvasRefreshProbe {
|
||||
private(set) var calls = 0
|
||||
|
||||
func refresh() -> String? {
|
||||
self.calls += 1
|
||||
return "http://127.0.0.1:18789/refreshed"
|
||||
}
|
||||
}
|
||||
|
||||
@Test func `handle invoke rejects unknown command`() async {
|
||||
let runtime = MacNodeRuntime()
|
||||
let response = await runtime.handleInvoke(
|
||||
@@ -12,6 +21,21 @@ struct MacNodeRuntimeTests {
|
||||
#expect(response.ok == false)
|
||||
}
|
||||
|
||||
@Test func `A2UI host capability refresh uses injected node session refresher`() async {
|
||||
let probe = CanvasRefreshProbe()
|
||||
let runtime = MacNodeRuntime(
|
||||
canvasSurfaceUrl: { "http://127.0.0.1:18789/current" },
|
||||
refreshCanvasSurfaceUrl: { await probe.refresh() })
|
||||
|
||||
let current = await runtime.resolveA2UIHostUrlWithCapabilityRefresh()
|
||||
#expect(current == "http://127.0.0.1:18789/current/__openclaw__/a2ui/?platform=macos")
|
||||
#expect(await probe.calls == 0)
|
||||
|
||||
let refreshed = await runtime.resolveA2UIHostUrlWithCapabilityRefresh(forceRefresh: true)
|
||||
#expect(refreshed == "http://127.0.0.1:18789/refreshed/__openclaw__/a2ui/?platform=macos")
|
||||
#expect(await probe.calls == 1)
|
||||
}
|
||||
|
||||
@Test func `handle invoke rejects empty system run`() async throws {
|
||||
let runtime = MacNodeRuntime()
|
||||
let params = OpenClawSystemRunParams(command: [])
|
||||
|
||||
@@ -9,8 +9,6 @@ import UniformTypeIdentifiers
|
||||
|
||||
@MainActor
|
||||
struct OpenClawChatComposer: View {
|
||||
private static let menuThinkingLevels = ["off", "low", "medium", "high"]
|
||||
|
||||
@Bindable var viewModel: OpenClawChatViewModel
|
||||
let style: OpenClawChatView.Style
|
||||
let showsSessionSwitcher: Bool
|
||||
@@ -95,12 +93,8 @@ struct OpenClawChatComposer: View {
|
||||
get: { self.viewModel.thinkingLevel },
|
||||
set: { next in self.viewModel.selectThinkingLevel(next) }))
|
||||
{
|
||||
Text("Off").tag("off")
|
||||
Text("Low").tag("low")
|
||||
Text("Medium").tag("medium")
|
||||
Text("High").tag("high")
|
||||
if !Self.menuThinkingLevels.contains(self.viewModel.thinkingLevel) {
|
||||
Text(self.viewModel.thinkingLevel.capitalized).tag(self.viewModel.thinkingLevel)
|
||||
ForEach(self.viewModel.thinkingLevelOptions) { option in
|
||||
Text(option.label).tag(option.id)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
public struct OpenClawChatThinkingLevelOption: Codable, Identifiable, Sendable, Hashable {
|
||||
public let id: String
|
||||
public let label: String
|
||||
|
||||
public init(id: String, label: String) {
|
||||
self.id = id
|
||||
self.label = label
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable {
|
||||
public var id: String {
|
||||
self.selectionID
|
||||
@@ -34,13 +44,29 @@ public struct OpenClawChatModelChoice: Identifiable, Codable, Sendable, Hashable
|
||||
}
|
||||
|
||||
public struct OpenClawChatSessionsDefaults: Codable, Sendable {
|
||||
public let modelProvider: String?
|
||||
public let model: String?
|
||||
public let contextTokens: Int?
|
||||
public let thinkingLevels: [OpenClawChatThinkingLevelOption]?
|
||||
public let thinkingOptions: [String]?
|
||||
public let thinkingDefault: String?
|
||||
public let mainSessionKey: String?
|
||||
|
||||
public init(model: String?, contextTokens: Int?, mainSessionKey: String? = nil) {
|
||||
public init(
|
||||
modelProvider: String? = nil,
|
||||
model: String?,
|
||||
contextTokens: Int?,
|
||||
thinkingLevels: [OpenClawChatThinkingLevelOption]? = nil,
|
||||
thinkingOptions: [String]? = nil,
|
||||
thinkingDefault: String? = nil,
|
||||
mainSessionKey: String? = nil)
|
||||
{
|
||||
self.modelProvider = modelProvider
|
||||
self.model = model
|
||||
self.contextTokens = contextTokens
|
||||
self.thinkingLevels = thinkingLevels
|
||||
self.thinkingOptions = thinkingOptions
|
||||
self.thinkingDefault = thinkingDefault
|
||||
self.mainSessionKey = mainSessionKey
|
||||
}
|
||||
}
|
||||
@@ -72,6 +98,57 @@ public struct OpenClawChatSessionEntry: Codable, Identifiable, Sendable, Hashabl
|
||||
public let modelProvider: String?
|
||||
public let model: String?
|
||||
public let contextTokens: Int?
|
||||
public let thinkingLevels: [OpenClawChatThinkingLevelOption]?
|
||||
public let thinkingOptions: [String]?
|
||||
public let thinkingDefault: String?
|
||||
|
||||
public init(
|
||||
key: String,
|
||||
kind: String?,
|
||||
displayName: String?,
|
||||
surface: String?,
|
||||
subject: String?,
|
||||
room: String?,
|
||||
space: String?,
|
||||
updatedAt: Double?,
|
||||
sessionId: String?,
|
||||
systemSent: Bool?,
|
||||
abortedLastRun: Bool?,
|
||||
thinkingLevel: String?,
|
||||
verboseLevel: String?,
|
||||
inputTokens: Int?,
|
||||
outputTokens: Int?,
|
||||
totalTokens: Int?,
|
||||
modelProvider: String?,
|
||||
model: String?,
|
||||
contextTokens: Int?,
|
||||
thinkingLevels: [OpenClawChatThinkingLevelOption]? = nil,
|
||||
thinkingOptions: [String]? = nil,
|
||||
thinkingDefault: String? = nil)
|
||||
{
|
||||
self.key = key
|
||||
self.kind = kind
|
||||
self.displayName = displayName
|
||||
self.surface = surface
|
||||
self.subject = subject
|
||||
self.room = room
|
||||
self.space = space
|
||||
self.updatedAt = updatedAt
|
||||
self.sessionId = sessionId
|
||||
self.systemSent = systemSent
|
||||
self.abortedLastRun = abortedLastRun
|
||||
self.thinkingLevel = thinkingLevel
|
||||
self.verboseLevel = verboseLevel
|
||||
self.inputTokens = inputTokens
|
||||
self.outputTokens = outputTokens
|
||||
self.totalTokens = totalTokens
|
||||
self.modelProvider = modelProvider
|
||||
self.model = model
|
||||
self.contextTokens = contextTokens
|
||||
self.thinkingLevels = thinkingLevels
|
||||
self.thinkingOptions = thinkingOptions
|
||||
self.thinkingDefault = thinkingDefault
|
||||
}
|
||||
}
|
||||
|
||||
public struct OpenClawChatSessionsListResponse: Codable, Sendable {
|
||||
|
||||
@@ -21,6 +21,7 @@ public final class OpenClawChatViewModel {
|
||||
public private(set) var messages: [OpenClawChatMessage] = []
|
||||
public var input: String = ""
|
||||
public private(set) var thinkingLevel: String
|
||||
public private(set) var thinkingLevelOptions: [OpenClawChatThinkingLevelOption]
|
||||
public private(set) var modelSelectionID: String = "__default__"
|
||||
public private(set) var modelChoices: [OpenClawChatModelChoice] = []
|
||||
public private(set) var isLoading = false
|
||||
@@ -83,7 +84,11 @@ public final class OpenClawChatViewModel {
|
||||
self.sessionKey = sessionKey
|
||||
self.transport = transport
|
||||
let normalizedThinkingLevel = Self.normalizedThinkingLevel(initialThinkingLevel)
|
||||
self.thinkingLevel = normalizedThinkingLevel ?? "off"
|
||||
let initialResolvedThinkingLevel = normalizedThinkingLevel ?? "off"
|
||||
self.thinkingLevel = initialResolvedThinkingLevel
|
||||
self.thinkingLevelOptions = Self.withCurrentThinkingOption(
|
||||
Self.baseThinkingLevelOptions,
|
||||
current: initialResolvedThinkingLevel)
|
||||
self.prefersExplicitThinkingLevel = normalizedThinkingLevel != nil
|
||||
self.onThinkingLevelChanged = onThinkingLevelChanged
|
||||
|
||||
@@ -198,6 +203,14 @@ public final class OpenClawChatViewModel {
|
||||
return "Default: \(self.modelLabel(for: defaultModelID))"
|
||||
}
|
||||
|
||||
private static let baseThinkingLevelOptions: [OpenClawChatThinkingLevelOption] = [
|
||||
OpenClawChatThinkingLevelOption(id: "off", label: "off"),
|
||||
OpenClawChatThinkingLevelOption(id: "minimal", label: "minimal"),
|
||||
OpenClawChatThinkingLevelOption(id: "low", label: "low"),
|
||||
OpenClawChatThinkingLevelOption(id: "medium", label: "medium"),
|
||||
OpenClawChatThinkingLevelOption(id: "high", label: "high"),
|
||||
]
|
||||
|
||||
public func addAttachments(urls: [URL]) {
|
||||
Task { await self.loadAttachments(urls: urls) }
|
||||
}
|
||||
@@ -243,6 +256,7 @@ public final class OpenClawChatViewModel {
|
||||
{
|
||||
self.thinkingLevel = level
|
||||
}
|
||||
self.syncThinkingLevelOptions()
|
||||
await self.pollHealthIfNeeded(force: true)
|
||||
await self.fetchSessions(limit: 50)
|
||||
await self.fetchModels()
|
||||
@@ -594,6 +608,7 @@ public final class OpenClawChatViewModel {
|
||||
self.sessions = res.sessions
|
||||
self.sessionDefaults = res.defaults
|
||||
self.syncSelectedModel()
|
||||
self.syncThinkingLevelOptions()
|
||||
} catch {
|
||||
// Best-effort.
|
||||
}
|
||||
@@ -675,6 +690,8 @@ public final class OpenClawChatViewModel {
|
||||
|
||||
let sessionKey = self.sessionKey
|
||||
self.thinkingLevel = next
|
||||
self.syncThinkingLevelOptions()
|
||||
self.updateCurrentSessionThinkingLevel(next, sessionKey: sessionKey)
|
||||
self.onThinkingLevelChanged?(next)
|
||||
self.nextThinkingSelectionRequestID &+= 1
|
||||
let requestID = self.nextThinkingSelectionRequestID
|
||||
@@ -770,6 +787,99 @@ public final class OpenClawChatViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private func syncThinkingLevelOptions() {
|
||||
let currentSession = self.sessions.first(where: { $0.key == self.sessionKey })
|
||||
var options = self.resolvedThinkingLevelOptions(for: currentSession)
|
||||
if let current = Self.normalizedThinkingLevel(self.thinkingLevel) {
|
||||
options = Self.withCurrentThinkingOption(options, current: current)
|
||||
}
|
||||
self.thinkingLevelOptions = options
|
||||
}
|
||||
|
||||
private func resolvedThinkingLevelOptions(
|
||||
for currentSession: OpenClawChatSessionEntry?) -> [OpenClawChatThinkingLevelOption]
|
||||
{
|
||||
if let levels = Self.normalizedThinkingLevelOptions(currentSession?.thinkingLevels), !levels.isEmpty {
|
||||
return levels
|
||||
}
|
||||
|
||||
let defaultsMatch = currentSession.map {
|
||||
Self.sessionModelMatchesDefaults($0, defaults: self.sessionDefaults)
|
||||
} ?? true
|
||||
|
||||
if defaultsMatch,
|
||||
let levels = Self.normalizedThinkingLevelOptions(self.sessionDefaults?.thinkingLevels),
|
||||
!levels.isEmpty
|
||||
{
|
||||
return levels
|
||||
}
|
||||
|
||||
if let options = Self.thinkingOptions(from: currentSession?.thinkingOptions), !options.isEmpty {
|
||||
return options
|
||||
}
|
||||
|
||||
if defaultsMatch,
|
||||
let options = Self.thinkingOptions(from: self.sessionDefaults?.thinkingOptions),
|
||||
!options.isEmpty
|
||||
{
|
||||
return options
|
||||
}
|
||||
|
||||
return Self.baseThinkingLevelOptions
|
||||
}
|
||||
|
||||
private static func sessionModelMatchesDefaults(
|
||||
_ session: OpenClawChatSessionEntry,
|
||||
defaults: OpenClawChatSessionsDefaults?) -> Bool
|
||||
{
|
||||
let providerMatches = session.modelProvider == nil || session.modelProvider == defaults?.modelProvider
|
||||
let modelMatches = session.model == nil || session.model == defaults?.model
|
||||
return providerMatches && modelMatches
|
||||
}
|
||||
|
||||
private static func normalizedThinkingLevelOptions(
|
||||
_ levels: [OpenClawChatThinkingLevelOption]?) -> [OpenClawChatThinkingLevelOption]?
|
||||
{
|
||||
guard let levels else { return nil }
|
||||
return Self.dedupedThinkingOptions(
|
||||
levels.compactMap { level in
|
||||
guard let id = Self.normalizedThinkingLevel(level.id) else { return nil }
|
||||
let label = level.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return OpenClawChatThinkingLevelOption(id: id, label: label.isEmpty ? id : label)
|
||||
})
|
||||
}
|
||||
|
||||
private static func thinkingOptions(from labels: [String]?) -> [OpenClawChatThinkingLevelOption]? {
|
||||
guard let labels else { return nil }
|
||||
return Self.dedupedThinkingOptions(
|
||||
labels.compactMap { label in
|
||||
guard let id = Self.normalizedThinkingLevel(label) else { return nil }
|
||||
let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return OpenClawChatThinkingLevelOption(id: id, label: trimmed.isEmpty ? id : trimmed)
|
||||
})
|
||||
}
|
||||
|
||||
private static func withCurrentThinkingOption(
|
||||
_ options: [OpenClawChatThinkingLevelOption],
|
||||
current: String) -> [OpenClawChatThinkingLevelOption]
|
||||
{
|
||||
guard !options.contains(where: { $0.id == current }) else { return options }
|
||||
return options + [OpenClawChatThinkingLevelOption(id: current, label: current)]
|
||||
}
|
||||
|
||||
private static func dedupedThinkingOptions(
|
||||
_ options: [OpenClawChatThinkingLevelOption]) -> [OpenClawChatThinkingLevelOption]
|
||||
{
|
||||
var result: [OpenClawChatThinkingLevelOption] = []
|
||||
var seen = Set<String>()
|
||||
for option in options {
|
||||
guard !option.id.isEmpty, !seen.contains(option.id) else { continue }
|
||||
seen.insert(option.id)
|
||||
result.append(option)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func placeholderSession(key: String) -> OpenClawChatSessionEntry {
|
||||
OpenClawChatSessionEntry(
|
||||
key: key,
|
||||
@@ -858,6 +968,9 @@ public final class OpenClawChatViewModel {
|
||||
modelProvider: resolved.modelProvider,
|
||||
sessionKey: sessionKey,
|
||||
syncSelection: syncSelection)
|
||||
if sessionKey == self.sessionKey {
|
||||
self.syncThinkingLevelOptions()
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvedSessionModelIdentity(forSelectionID selectionID: String)
|
||||
@@ -885,6 +998,34 @@ public final class OpenClawChatViewModel {
|
||||
return "\(provider)/\(modelID)"
|
||||
}
|
||||
|
||||
private func updateCurrentSessionThinkingLevel(_ thinkingLevel: String?, sessionKey: String) {
|
||||
guard let index = self.sessions.firstIndex(where: { $0.key == sessionKey }) else { return }
|
||||
let current = self.sessions[index]
|
||||
self.sessions[index] = OpenClawChatSessionEntry(
|
||||
key: current.key,
|
||||
kind: current.kind,
|
||||
displayName: current.displayName,
|
||||
surface: current.surface,
|
||||
subject: current.subject,
|
||||
room: current.room,
|
||||
space: current.space,
|
||||
updatedAt: current.updatedAt,
|
||||
sessionId: current.sessionId,
|
||||
systemSent: current.systemSent,
|
||||
abortedLastRun: current.abortedLastRun,
|
||||
thinkingLevel: thinkingLevel,
|
||||
verboseLevel: current.verboseLevel,
|
||||
inputTokens: current.inputTokens,
|
||||
outputTokens: current.outputTokens,
|
||||
totalTokens: current.totalTokens,
|
||||
modelProvider: current.modelProvider,
|
||||
model: current.model,
|
||||
contextTokens: current.contextTokens,
|
||||
thinkingLevels: current.thinkingLevels,
|
||||
thinkingOptions: current.thinkingOptions,
|
||||
thinkingDefault: current.thinkingDefault)
|
||||
}
|
||||
|
||||
private func updateCurrentSessionModel(
|
||||
modelID: String?,
|
||||
modelProvider: String?,
|
||||
@@ -1084,6 +1225,7 @@ public final class OpenClawChatViewModel {
|
||||
let level = Self.normalizedThinkingLevel(payload.thinkingLevel)
|
||||
{
|
||||
self.thinkingLevel = level
|
||||
self.syncThinkingLevelOptions()
|
||||
}
|
||||
} catch {
|
||||
chatUILogger.error("refresh history failed \(error.localizedDescription, privacy: .public)")
|
||||
@@ -1195,9 +1337,33 @@ public final class OpenClawChatViewModel {
|
||||
private static func normalizedThinkingLevel(_ level: String?) -> String? {
|
||||
guard let level else { return nil }
|
||||
let trimmed = level.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
guard ["off", "minimal", "low", "medium", "high", "xhigh", "adaptive"].contains(trimmed) else {
|
||||
return nil
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
let collapsed = trimmed.replacingOccurrences(
|
||||
of: "[\\s_-]+",
|
||||
with: "",
|
||||
options: .regularExpression)
|
||||
|
||||
switch collapsed {
|
||||
case "adaptive", "auto":
|
||||
return "adaptive"
|
||||
case "max":
|
||||
return "max"
|
||||
case "xhigh", "extrahigh":
|
||||
return "xhigh"
|
||||
case "off", "none":
|
||||
return "off"
|
||||
case "on", "enable", "enabled":
|
||||
return "low"
|
||||
case "min", "minimal", "think":
|
||||
return "minimal"
|
||||
case "low", "thinkhard":
|
||||
return "low"
|
||||
case "mid", "med", "medium", "thinkharder", "harder":
|
||||
return "medium"
|
||||
case "high", "ultra", "ultrathink", "thinkhardest", "highest":
|
||||
return "high"
|
||||
default:
|
||||
return trimmed
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,18 +105,15 @@ public struct BridgeHello: Codable, Sendable {
|
||||
public struct BridgeHelloOk: Codable, Sendable {
|
||||
public let type: String
|
||||
public let serverName: String
|
||||
public let canvasHostUrl: String?
|
||||
public let mainSessionKey: String?
|
||||
|
||||
public init(
|
||||
type: String = "hello-ok",
|
||||
serverName: String,
|
||||
canvasHostUrl: String? = nil,
|
||||
mainSessionKey: String? = nil)
|
||||
{
|
||||
self.type = type
|
||||
self.serverName = serverName
|
||||
self.canvasHostUrl = canvasHostUrl
|
||||
self.mainSessionKey = mainSessionKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,6 @@ private struct NodeInvokeRequestPayload: Codable {
|
||||
var idempotencyKey: String?
|
||||
}
|
||||
|
||||
private func replaceCanvasCapabilityInScopedHostUrl(scopedUrl: String, capability: String) -> String? {
|
||||
let marker = "/__openclaw__/cap/"
|
||||
guard let markerRange = scopedUrl.range(of: marker) else { return nil }
|
||||
let capabilityStart = markerRange.upperBound
|
||||
let suffix = scopedUrl[capabilityStart...]
|
||||
let nextSlash = suffix.firstIndex(of: "/")
|
||||
let nextQuery = suffix.firstIndex(of: "?")
|
||||
let nextFragment = suffix.firstIndex(of: "#")
|
||||
let capabilityEnd = [nextSlash, nextQuery, nextFragment].compactMap(\.self).min() ?? scopedUrl.endIndex
|
||||
guard capabilityStart < capabilityEnd else { return nil }
|
||||
return String(scopedUrl[..<capabilityStart]) + capability + String(scopedUrl[capabilityEnd...])
|
||||
}
|
||||
|
||||
func canonicalizeCanvasHostUrl(raw: String?, activeURL: URL?) -> String? {
|
||||
let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
@@ -152,7 +139,11 @@ public actor GatewayNodeSession {
|
||||
}
|
||||
|
||||
private var serverEventSubscribers: [UUID: AsyncStream<EventFrame>.Continuation] = [:]
|
||||
private var canvasHostUrl: String?
|
||||
private var pluginSurfaceUrls: [String: String] = [:]
|
||||
|
||||
private struct PluginSurfaceRefreshResponse: Decodable {
|
||||
let pluginSurfaceUrls: [String: AnyCodable]?
|
||||
}
|
||||
|
||||
public init() {}
|
||||
|
||||
@@ -270,47 +261,26 @@ public actor GatewayNodeSession {
|
||||
}
|
||||
|
||||
public func currentCanvasHostUrl() -> String? {
|
||||
self.canvasHostUrl
|
||||
self.pluginSurfaceUrls["canvas"]
|
||||
}
|
||||
|
||||
public func refreshNodeCanvasCapability(timeoutMs: Int = 8000) async -> Bool {
|
||||
guard let channel = self.channel else { return false }
|
||||
do {
|
||||
let data = try await channel.request(
|
||||
method: "node.canvas.capability.refresh",
|
||||
params: [:],
|
||||
timeoutMs: Double(max(timeoutMs, 1)))
|
||||
guard
|
||||
let payload = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let rawCapability = payload["canvasCapability"] as? String
|
||||
else {
|
||||
self.logger.warning("node.canvas.capability.refresh missing canvasCapability")
|
||||
return false
|
||||
}
|
||||
let capability = rawCapability.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !capability.isEmpty else {
|
||||
self.logger.warning("node.canvas.capability.refresh returned empty capability")
|
||||
return false
|
||||
}
|
||||
let scopedUrl = self.canvasHostUrl?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
guard !scopedUrl.isEmpty else {
|
||||
self.logger.warning("node.canvas.capability.refresh missing local canvasHostUrl")
|
||||
return false
|
||||
}
|
||||
guard let refreshed = replaceCanvasCapabilityInScopedHostUrl(
|
||||
scopedUrl: scopedUrl,
|
||||
capability: capability)
|
||||
else {
|
||||
self.logger.warning("node.canvas.capability.refresh could not rewrite scoped canvas URL")
|
||||
return false
|
||||
}
|
||||
self.canvasHostUrl = refreshed
|
||||
return true
|
||||
} catch {
|
||||
self.logger.warning(
|
||||
"node.canvas.capability.refresh failed: \(error.localizedDescription, privacy: .public)")
|
||||
return false
|
||||
}
|
||||
@discardableResult
|
||||
public func refreshPluginSurfaceUrl(surface: String, timeoutSeconds: Int = 8) async -> String? {
|
||||
guard let channel = self.channel else { return nil }
|
||||
let trimmedSurface = surface.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmedSurface.isEmpty else { return nil }
|
||||
|
||||
return await self.requestPluginSurfaceRefresh(
|
||||
channel: channel,
|
||||
method: "node.pluginSurface.refresh",
|
||||
params: ["surface": AnyCodable(trimmedSurface)],
|
||||
surface: trimmedSurface,
|
||||
timeoutSeconds: timeoutSeconds)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func refreshCanvasHostUrl(timeoutSeconds: Int = 8) async -> String? {
|
||||
await self.refreshPluginSurfaceUrl(surface: "canvas", timeoutSeconds: timeoutSeconds)
|
||||
}
|
||||
|
||||
public func currentRemoteAddress() -> String? {
|
||||
@@ -364,8 +334,7 @@ public actor GatewayNodeSession {
|
||||
private func handlePush(_ push: GatewayPush) async {
|
||||
switch push {
|
||||
case let .snapshot(ok):
|
||||
let raw = ok.canvashosturl?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.canvasHostUrl = self.normalizeCanvasHostUrl(raw)
|
||||
self.pluginSurfaceUrls = self.normalizePluginSurfaceUrls(ok.pluginsurfaceurls)
|
||||
if self.hasEverConnected {
|
||||
self.broadcastServerEvent(
|
||||
EventFrame(type: "event", event: "seqGap", payload: nil, seq: nil, stateversion: nil))
|
||||
@@ -436,6 +405,39 @@ public actor GatewayNodeSession {
|
||||
canonicalizeCanvasHostUrl(raw: raw, activeURL: self.activeURL)
|
||||
}
|
||||
|
||||
private func normalizePluginSurfaceUrls(_ raw: [String: AnyCodable]?) -> [String: String] {
|
||||
var normalized: [String: String] = [:]
|
||||
if let raw {
|
||||
normalized = raw.compactMapValues { value in
|
||||
self.normalizeCanvasHostUrl(value.value as? String)
|
||||
}
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
private func requestPluginSurfaceRefresh(
|
||||
channel: GatewayChannelActor,
|
||||
method: String,
|
||||
params: [String: AnyCodable]?,
|
||||
surface: String,
|
||||
timeoutSeconds: Int) async -> String?
|
||||
{
|
||||
do {
|
||||
let data = try await channel.request(
|
||||
method: method,
|
||||
params: params,
|
||||
timeoutMs: Double(timeoutSeconds * 1000))
|
||||
let decoded = try self.decoder.decode(PluginSurfaceRefreshResponse.self, from: data)
|
||||
let urls = self.normalizePluginSurfaceUrls(decoded.pluginSurfaceUrls)
|
||||
guard let refreshed = urls[surface] else { return nil }
|
||||
self.pluginSurfaceUrls[surface] = refreshed
|
||||
return refreshed
|
||||
} catch {
|
||||
self.logger.debug("\(method, privacy: .public) failed: \(error.localizedDescription, privacy: .public)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func handleEvent(_ evt: EventFrame) async {
|
||||
self.broadcastServerEvent(evt)
|
||||
guard evt.event == "node.invoke.request" else { return }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// swiftlint:disable file_length
|
||||
import Foundation
|
||||
|
||||
public let GATEWAY_PROTOCOL_VERSION = 3
|
||||
public let GATEWAY_PROTOCOL_VERSION = 4
|
||||
|
||||
public enum ErrorCode: String, Codable, Sendable {
|
||||
case notLinked = "NOT_LINKED"
|
||||
@@ -98,7 +98,7 @@ public struct HelloOk: Codable, Sendable {
|
||||
public let server: [String: AnyCodable]
|
||||
public let features: [String: AnyCodable]
|
||||
public let snapshot: Snapshot
|
||||
public let canvashosturl: String?
|
||||
public let pluginsurfaceurls: [String: AnyCodable]?
|
||||
public let auth: [String: AnyCodable]
|
||||
public let policy: [String: AnyCodable]
|
||||
|
||||
@@ -108,7 +108,7 @@ public struct HelloOk: Codable, Sendable {
|
||||
server: [String: AnyCodable],
|
||||
features: [String: AnyCodable],
|
||||
snapshot: Snapshot,
|
||||
canvashosturl: String?,
|
||||
pluginsurfaceurls: [String: AnyCodable]?,
|
||||
auth: [String: AnyCodable],
|
||||
policy: [String: AnyCodable])
|
||||
{
|
||||
@@ -117,7 +117,7 @@ public struct HelloOk: Codable, Sendable {
|
||||
self.server = server
|
||||
self.features = features
|
||||
self.snapshot = snapshot
|
||||
self.canvashosturl = canvashosturl
|
||||
self.pluginsurfaceurls = pluginsurfaceurls
|
||||
self.auth = auth
|
||||
self.policy = policy
|
||||
}
|
||||
@@ -128,7 +128,7 @@ public struct HelloOk: Codable, Sendable {
|
||||
case server
|
||||
case features
|
||||
case snapshot
|
||||
case canvashosturl = "canvasHostUrl"
|
||||
case pluginsurfaceurls = "pluginSurfaceUrls"
|
||||
case auth
|
||||
case policy
|
||||
}
|
||||
@@ -1517,6 +1517,7 @@ public struct SessionsListParams: Codable, Sendable {
|
||||
public let activeminutes: Int?
|
||||
public let includeglobal: Bool?
|
||||
public let includeunknown: Bool?
|
||||
public let configuredagentsonly: Bool?
|
||||
public let includederivedtitles: Bool?
|
||||
public let includelastmessage: Bool?
|
||||
public let label: String?
|
||||
@@ -1529,6 +1530,7 @@ public struct SessionsListParams: Codable, Sendable {
|
||||
activeminutes: Int?,
|
||||
includeglobal: Bool?,
|
||||
includeunknown: Bool?,
|
||||
configuredagentsonly: Bool?,
|
||||
includederivedtitles: Bool?,
|
||||
includelastmessage: Bool?,
|
||||
label: String?,
|
||||
@@ -1540,6 +1542,7 @@ public struct SessionsListParams: Codable, Sendable {
|
||||
self.activeminutes = activeminutes
|
||||
self.includeglobal = includeglobal
|
||||
self.includeunknown = includeunknown
|
||||
self.configuredagentsonly = configuredagentsonly
|
||||
self.includederivedtitles = includederivedtitles
|
||||
self.includelastmessage = includelastmessage
|
||||
self.label = label
|
||||
@@ -1553,6 +1556,7 @@ public struct SessionsListParams: Codable, Sendable {
|
||||
case activeminutes = "activeMinutes"
|
||||
case includeglobal = "includeGlobal"
|
||||
case includeunknown = "includeUnknown"
|
||||
case configuredagentsonly = "configuredAgentsOnly"
|
||||
case includederivedtitles = "includeDerivedTitles"
|
||||
case includelastmessage = "includeLastMessage"
|
||||
case label
|
||||
@@ -1568,19 +1572,22 @@ public struct SessionsCleanupParams: Codable, Sendable {
|
||||
public let enforce: Bool?
|
||||
public let activekey: String?
|
||||
public let fixmissing: Bool?
|
||||
public let fixdmscope: Bool?
|
||||
|
||||
public init(
|
||||
agent: String?,
|
||||
allagents: Bool?,
|
||||
enforce: Bool?,
|
||||
activekey: String?,
|
||||
fixmissing: Bool?)
|
||||
fixmissing: Bool?,
|
||||
fixdmscope: Bool?)
|
||||
{
|
||||
self.agent = agent
|
||||
self.allagents = allagents
|
||||
self.enforce = enforce
|
||||
self.activekey = activekey
|
||||
self.fixmissing = fixmissing
|
||||
self.fixdmscope = fixdmscope
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
@@ -1589,6 +1596,7 @@ public struct SessionsCleanupParams: Codable, Sendable {
|
||||
case enforce
|
||||
case activekey = "activeKey"
|
||||
case fixmissing = "fixMissing"
|
||||
case fixdmscope = "fixDmScope"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@ private func sessionEntry(key: String, updatedAt: Double) -> OpenClawChatSession
|
||||
contextTokens: nil)
|
||||
}
|
||||
|
||||
private func thinkingOption(_ id: String, label: String? = nil) -> OpenClawChatThinkingLevelOption {
|
||||
OpenClawChatThinkingLevelOption(id: id, label: label ?? id)
|
||||
}
|
||||
|
||||
private func sessionEntry(
|
||||
key: String,
|
||||
updatedAt: Double,
|
||||
@@ -1632,6 +1636,272 @@ extension TestChatTransportState {
|
||||
}
|
||||
}
|
||||
|
||||
@Test func decodesGatewayThinkingMetadataFromSessionList() throws {
|
||||
let json = """
|
||||
{
|
||||
"defaults": {
|
||||
"modelProvider": "anthropic",
|
||||
"model": "claude-opus-4-7",
|
||||
"thinkingLevels": [
|
||||
{ "id": "off", "label": "off" },
|
||||
{ "id": "adaptive", "label": "adaptive" },
|
||||
{ "id": "max", "label": "maximum" }
|
||||
],
|
||||
"thinkingOptions": ["off", "adaptive", "maximum"],
|
||||
"thinkingDefault": "adaptive"
|
||||
},
|
||||
"sessions": [
|
||||
{
|
||||
"key": "main",
|
||||
"modelProvider": "openrouter",
|
||||
"model": "deepseek/deepseek-v4",
|
||||
"thinkingLevel": "max",
|
||||
"thinkingLevels": [
|
||||
{ "id": "off", "label": "off" },
|
||||
{ "id": "xhigh", "label": "xhigh" },
|
||||
{ "id": "max", "label": "max" }
|
||||
],
|
||||
"thinkingOptions": ["off", "xhigh", "max"],
|
||||
"thinkingDefault": "max"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
let decoded = try JSONDecoder().decode(
|
||||
OpenClawChatSessionsListResponse.self,
|
||||
from: Data(json.utf8))
|
||||
|
||||
#expect(decoded.defaults?.modelProvider == "anthropic")
|
||||
#expect(decoded.defaults?.thinkingLevels?.map(\.id) == ["off", "adaptive", "max"])
|
||||
#expect(decoded.defaults?.thinkingLevels?.last?.label == "maximum")
|
||||
#expect(decoded.defaults?.thinkingDefault == "adaptive")
|
||||
#expect(decoded.sessions.first?.thinkingLevels?.map(\.id) == ["off", "xhigh", "max"])
|
||||
#expect(decoded.sessions.first?.thinkingDefault == "max")
|
||||
}
|
||||
|
||||
@Test func sessionThinkingLevelsDrivePickerOptions() async throws {
|
||||
let history = OpenClawChatHistoryPayload(
|
||||
sessionKey: "main",
|
||||
sessionId: "sess-main",
|
||||
messages: [],
|
||||
thinkingLevel: "adaptive")
|
||||
let sessions = OpenClawChatSessionsListResponse(
|
||||
ts: 1,
|
||||
path: nil,
|
||||
count: 1,
|
||||
defaults: OpenClawChatSessionsDefaults(
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.5",
|
||||
contextTokens: nil,
|
||||
thinkingLevels: [
|
||||
thinkingOption("off"),
|
||||
thinkingOption("low"),
|
||||
thinkingOption("xhigh"),
|
||||
thinkingOption("max", label: "maximum"),
|
||||
],
|
||||
thinkingOptions: ["off", "low", "xhigh", "maximum"],
|
||||
thinkingDefault: "xhigh"),
|
||||
sessions: [
|
||||
OpenClawChatSessionEntry(
|
||||
key: "main",
|
||||
kind: nil,
|
||||
displayName: nil,
|
||||
surface: nil,
|
||||
subject: nil,
|
||||
room: nil,
|
||||
space: nil,
|
||||
updatedAt: 1,
|
||||
sessionId: "sess-main",
|
||||
systemSent: nil,
|
||||
abortedLastRun: nil,
|
||||
thinkingLevel: "adaptive",
|
||||
verboseLevel: nil,
|
||||
inputTokens: nil,
|
||||
outputTokens: nil,
|
||||
totalTokens: nil,
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-opus-4-7",
|
||||
contextTokens: nil,
|
||||
thinkingLevels: [
|
||||
thinkingOption("off"),
|
||||
thinkingOption("adaptive"),
|
||||
thinkingOption("max", label: "maximum"),
|
||||
],
|
||||
thinkingOptions: ["off", "adaptive", "maximum"],
|
||||
thinkingDefault: "adaptive"),
|
||||
])
|
||||
|
||||
let (_, vm) = await makeViewModel(
|
||||
historyResponses: [history],
|
||||
sessionsResponses: [sessions])
|
||||
|
||||
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
|
||||
|
||||
#expect(await MainActor.run { vm.thinkingLevel } == "adaptive")
|
||||
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "adaptive", "max"])
|
||||
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.label) } == ["off", "adaptive", "maximum"])
|
||||
}
|
||||
|
||||
@Test func thinkingOptionsFallbackAndCurrentUnsupportedLevelStayVisible() async throws {
|
||||
let history = OpenClawChatHistoryPayload(
|
||||
sessionKey: "main",
|
||||
sessionId: "sess-main",
|
||||
messages: [],
|
||||
thinkingLevel: "xhigh")
|
||||
let sessions = OpenClawChatSessionsListResponse(
|
||||
ts: 1,
|
||||
path: nil,
|
||||
count: 1,
|
||||
defaults: nil,
|
||||
sessions: [
|
||||
OpenClawChatSessionEntry(
|
||||
key: "main",
|
||||
kind: nil,
|
||||
displayName: nil,
|
||||
surface: nil,
|
||||
subject: nil,
|
||||
room: nil,
|
||||
space: nil,
|
||||
updatedAt: 1,
|
||||
sessionId: "sess-main",
|
||||
systemSent: nil,
|
||||
abortedLastRun: nil,
|
||||
thinkingLevel: "xhigh",
|
||||
verboseLevel: nil,
|
||||
inputTokens: nil,
|
||||
outputTokens: nil,
|
||||
totalTokens: nil,
|
||||
modelProvider: "openrouter",
|
||||
model: "deepseek/deepseek-v4",
|
||||
contextTokens: nil,
|
||||
thinkingLevels: nil,
|
||||
thinkingOptions: ["off", "max"],
|
||||
thinkingDefault: "max"),
|
||||
])
|
||||
|
||||
let (_, vm) = await makeViewModel(
|
||||
historyResponses: [history],
|
||||
sessionsResponses: [sessions])
|
||||
|
||||
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
|
||||
|
||||
#expect(await MainActor.run { vm.thinkingLevel } == "xhigh")
|
||||
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "max", "xhigh"])
|
||||
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.label) } == ["off", "max", "xhigh"])
|
||||
}
|
||||
|
||||
@Test func matchingDefaultThinkingLevelsBeatLegacyRowThinkingOptions() async throws {
|
||||
let history = OpenClawChatHistoryPayload(
|
||||
sessionKey: "main",
|
||||
sessionId: "sess-main",
|
||||
messages: [],
|
||||
thinkingLevel: "adaptive")
|
||||
let sessions = OpenClawChatSessionsListResponse(
|
||||
ts: 1,
|
||||
path: nil,
|
||||
count: 1,
|
||||
defaults: OpenClawChatSessionsDefaults(
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-opus-4-7",
|
||||
contextTokens: nil,
|
||||
thinkingLevels: [
|
||||
thinkingOption("off"),
|
||||
thinkingOption("adaptive"),
|
||||
thinkingOption("max"),
|
||||
],
|
||||
thinkingOptions: ["off", "adaptive", "max"],
|
||||
thinkingDefault: "adaptive"),
|
||||
sessions: [
|
||||
OpenClawChatSessionEntry(
|
||||
key: "main",
|
||||
kind: nil,
|
||||
displayName: nil,
|
||||
surface: nil,
|
||||
subject: nil,
|
||||
room: nil,
|
||||
space: nil,
|
||||
updatedAt: 1,
|
||||
sessionId: "sess-main",
|
||||
systemSent: nil,
|
||||
abortedLastRun: nil,
|
||||
thinkingLevel: "adaptive",
|
||||
verboseLevel: nil,
|
||||
inputTokens: nil,
|
||||
outputTokens: nil,
|
||||
totalTokens: nil,
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-opus-4-7",
|
||||
contextTokens: nil,
|
||||
thinkingLevels: nil,
|
||||
thinkingOptions: ["off"],
|
||||
thinkingDefault: "off"),
|
||||
])
|
||||
|
||||
let (_, vm) = await makeViewModel(
|
||||
historyResponses: [history],
|
||||
sessionsResponses: [sessions])
|
||||
|
||||
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
|
||||
|
||||
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } == ["off", "adaptive", "max"])
|
||||
}
|
||||
|
||||
@Test func defaultThinkingLevelsDoNotLeakToDifferentSessionModel() async throws {
|
||||
let history = OpenClawChatHistoryPayload(
|
||||
sessionKey: "main",
|
||||
sessionId: "sess-main",
|
||||
messages: [],
|
||||
thinkingLevel: "max")
|
||||
let sessions = OpenClawChatSessionsListResponse(
|
||||
ts: 1,
|
||||
path: nil,
|
||||
count: 1,
|
||||
defaults: OpenClawChatSessionsDefaults(
|
||||
modelProvider: "anthropic",
|
||||
model: "claude-opus-4-7",
|
||||
contextTokens: nil,
|
||||
thinkingLevels: [
|
||||
thinkingOption("off"),
|
||||
thinkingOption("adaptive"),
|
||||
thinkingOption("max"),
|
||||
],
|
||||
thinkingOptions: ["off", "adaptive", "max"],
|
||||
thinkingDefault: "adaptive"),
|
||||
sessions: [
|
||||
OpenClawChatSessionEntry(
|
||||
key: "main",
|
||||
kind: nil,
|
||||
displayName: nil,
|
||||
surface: nil,
|
||||
subject: nil,
|
||||
room: nil,
|
||||
space: nil,
|
||||
updatedAt: 1,
|
||||
sessionId: "sess-main",
|
||||
systemSent: nil,
|
||||
abortedLastRun: nil,
|
||||
thinkingLevel: "max",
|
||||
verboseLevel: nil,
|
||||
inputTokens: nil,
|
||||
outputTokens: nil,
|
||||
totalTokens: nil,
|
||||
modelProvider: "openai",
|
||||
model: "gpt-5.4",
|
||||
contextTokens: nil),
|
||||
])
|
||||
|
||||
let (_, vm) = await makeViewModel(
|
||||
historyResponses: [history],
|
||||
sessionsResponses: [sessions])
|
||||
|
||||
try await loadAndWaitBootstrap(vm: vm, sessionId: "sess-main")
|
||||
|
||||
#expect(await MainActor.run { vm.thinkingLevel } == "max")
|
||||
#expect(await MainActor.run { vm.thinkingLevelOptions.map(\.id) } ==
|
||||
["off", "minimal", "low", "medium", "high", "max"])
|
||||
}
|
||||
|
||||
@Test func staleThinkingPatchCompletionReappliesLatestSelection() async throws {
|
||||
let history = OpenClawChatHistoryPayload(
|
||||
sessionKey: "main",
|
||||
|
||||
@@ -9,6 +9,7 @@ const rootEntries = [
|
||||
"src/index.ts!",
|
||||
"src/entry.ts!",
|
||||
"src/cli/daemon-cli.ts!",
|
||||
"src/infra/kysely-node-sqlite.ts!",
|
||||
"src/infra/warning-filter.ts!",
|
||||
"src/infra/command-explainer/index.ts!",
|
||||
bundledPluginFile("telegram", "src/audit.ts", "!"),
|
||||
@@ -30,10 +31,12 @@ const bundledPluginEntries = [
|
||||
|
||||
const bundledPluginIgnoredRuntimeDependencies = [
|
||||
"@agentclientprotocol/claude-agent-acp",
|
||||
"@a2ui/lit",
|
||||
"@azure/identity",
|
||||
"@clawdbot/lobster",
|
||||
"@discordjs/opus",
|
||||
"@homebridge/ciao",
|
||||
"@lit/context",
|
||||
"@matrix-org/matrix-sdk-crypto-wasm",
|
||||
"@mozilla/readability",
|
||||
"@openai/codex",
|
||||
@@ -42,6 +45,7 @@ const bundledPluginIgnoredRuntimeDependencies = [
|
||||
"@zed-industries/codex-acp",
|
||||
"jiti",
|
||||
"json5",
|
||||
"lit",
|
||||
"linkedom",
|
||||
"openclaw",
|
||||
"pdfjs-dist",
|
||||
@@ -169,7 +173,7 @@ const config = {
|
||||
// Bundled plugins often load their public surface via string specifiers in
|
||||
// `index.ts` contracts, so Knip needs these convention-based entry files.
|
||||
entry: bundledPluginEntries,
|
||||
project: ["index.ts!", "src/**/*.ts!"],
|
||||
project: ["index.ts!", "src/**/*.{js,mjs,ts}!"],
|
||||
ignoreDependencies: bundledPluginIgnoredRuntimeDependencies,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
da2ba9afd1062db1fafe81fb42e39db4ad65995a5e56caef4057a9954c2c386b config-baseline.json
|
||||
f860a7d43d3bd15379d8c3dfccbc6fcbf47b9bec8d8b67b29dd7313946905645 config-baseline.core.json
|
||||
cd7c0c7fb1435bc7e59099e9ac334462d5ad444016e9ab4512aae63a238f78dc config-baseline.channel.json
|
||||
2fee9c16a60d074fac428b4ad14c38ad3ca7febefacfad819f741a820101326e config-baseline.plugin.json
|
||||
885a734aa93cf04f6c14f8d83c1e96a66a5b96705327ea2de7b2aa7314238976 config-baseline.json
|
||||
074eb9a1480ff40836d98090ccb9be3465345ac4b46e0d273b7995504bbb8008 config-baseline.core.json
|
||||
ed15b24c1ccf0234e6b3435149a6f1c1e709579d1259f1d09402688799b149bd config-baseline.channel.json
|
||||
c4e8d8898eebc4d40f35b167c987870e426e6c82121696dc055ff929f6a24046 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
83c7b0a2953a24cac8d576bb561948ccd70d4bac3c06d0a39814a766b7a330b6 plugin-sdk-api-baseline.json
|
||||
387c0a4b34b0edd3c576658d71f13cdeb64d74bc949c36698798563de08f570d plugin-sdk-api-baseline.jsonl
|
||||
fecac0023b0a8de6334740483ef03500c72f3235e5b636e089bf581b00e8734a plugin-sdk-api-baseline.json
|
||||
b427b2c8bddefb6c0ab4f411065adeec230d1e126a792ed30e6d0a45053dd4e3 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -3,6 +3,26 @@
|
||||
"source": "OpenClaw",
|
||||
"target": "OpenClaw"
|
||||
},
|
||||
{
|
||||
"source": "iMessage",
|
||||
"target": "iMessage"
|
||||
},
|
||||
{
|
||||
"source": "Coming from BlueBubbles",
|
||||
"target": "Coming from BlueBubbles"
|
||||
},
|
||||
{
|
||||
"source": "BlueBubbles",
|
||||
"target": "BlueBubbles"
|
||||
},
|
||||
{
|
||||
"source": "Pairing",
|
||||
"target": "配对"
|
||||
},
|
||||
{
|
||||
"source": "Channel Routing",
|
||||
"target": "频道路由"
|
||||
},
|
||||
{
|
||||
"source": "ClawHub",
|
||||
"target": "ClawHub"
|
||||
@@ -31,6 +51,10 @@
|
||||
"source": "Message lifecycle refactor",
|
||||
"target": "消息生命周期重构"
|
||||
},
|
||||
{
|
||||
"source": "ACP lifecycle refactor",
|
||||
"target": "ACP 生命周期重构"
|
||||
},
|
||||
{
|
||||
"source": "Channel message API",
|
||||
"target": "频道消息 API"
|
||||
|
||||
@@ -76,7 +76,6 @@
|
||||
{
|
||||
"group": "消息平台",
|
||||
"pages": [
|
||||
"zh-CN/channels/bluebubbles",
|
||||
"zh-CN/channels/discord",
|
||||
"zh-CN/channels/feishu",
|
||||
"zh-CN/channels/grammy",
|
||||
@@ -204,7 +203,6 @@
|
||||
"zh-CN/tools/slash-commands",
|
||||
"zh-CN/tools/skills",
|
||||
"zh-CN/tools/skills-config",
|
||||
"zh-CN/tools/clawhub",
|
||||
"zh-CN/tools/plugin"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -62,6 +62,18 @@ Explicit copy flows, such as `openclaw agents add`, use this portability policy:
|
||||
Non-portable profiles remain available through read-through inheritance unless
|
||||
the target agent signs in separately and creates its own local profile.
|
||||
|
||||
## Config-only auth routes
|
||||
|
||||
`auth.profiles` entries with `mode: "aws-sdk"` are routing metadata, not stored
|
||||
credentials. They are valid when the target provider uses
|
||||
`models.providers.<id>.auth: "aws-sdk"` or the built-in Amazon Bedrock default
|
||||
AWS SDK route. These profile ids may appear in `auth.order` and session
|
||||
overrides even when no matching entry exists in `auth-profiles.json`.
|
||||
|
||||
Do not write `type: "aws-sdk"` into `auth-profiles.json`. If a legacy install
|
||||
has such a marker, `openclaw doctor --fix` moves it to `auth.profiles` and
|
||||
removes the marker from the credential store.
|
||||
|
||||
## Explicit auth order filtering
|
||||
|
||||
- When `auth.order.<provider>` or the auth-store order override is set for a
|
||||
|
||||
@@ -134,8 +134,6 @@ This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw use
|
||||
|
||||
`--model` uses the selected allowed model as that job's primary model. It is not the same as a chat-session `/model` override: configured fallback chains still apply when the job primary fails. If the requested model is not allowed or cannot be resolved, cron fails the run with an explicit validation error instead of silently falling back to the job's agent/default model selection.
|
||||
|
||||
If older or hand-edited `jobs.json` entries store `payload.model` as `"default"`, `"null"`, a blank string, or JSON `null`, run `openclaw doctor --fix`. Doctor removes those invalid persisted override sentinels; runtime does not support them as fallback aliases. Omit the model field to use the normal agent/default model selection.
|
||||
|
||||
Cron jobs can also carry payload-level `fallbacks`. When present, that list replaces the configured fallback chain for the job. Use `fallbacks: []` in the job payload/API when you want a strict cron run that tries only the selected model. If a job has `--model` but neither payload nor configured fallbacks, OpenClaw passes an explicit empty fallback override so the agent primary is not appended as a hidden extra retry target.
|
||||
|
||||
Model-selection precedence for isolated jobs is:
|
||||
|
||||
@@ -90,7 +90,7 @@ openclaw cron add \
|
||||
--tz America/New_York \
|
||||
--timeout-seconds 300 \
|
||||
--announce \
|
||||
--channel bluebubbles \
|
||||
--channel imessage \
|
||||
--to "+1XXXXXXXXXX" \
|
||||
--message "Execute daily inbox triage per standing orders. Check mail for new alerts. Parse, categorize, and persist each item. Report summary to owner. Escalate unknowns."
|
||||
```
|
||||
|
||||
@@ -90,7 +90,7 @@ Recommended data provenance fields for every collected item:
|
||||
|
||||
Have the workflow reject or mark stale items before summarization. The LLM step should receive only structured JSON and should be asked to preserve `sourceUrl`, `retrievedAt`, and `asOf` in its output. Use [LLM Task](/tools/llm-task) when you need a schema-validated model step inside the workflow.
|
||||
|
||||
For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/tools/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability.
|
||||
For reusable team or community workflows, package the CLI, `.lobster` files, and any setup notes as a skill or plugin and publish it through [ClawHub](/clawhub). Keep workflow-specific guardrails in that package unless the plugin API is missing a needed generic capability.
|
||||
|
||||
## Sync modes
|
||||
|
||||
|
||||
@@ -1,638 +0,0 @@
|
||||
---
|
||||
summary: "Legacy iMessage support via the BlueBubbles macOS server (REST send/receive, typing, reactions, pairing, advanced actions)."
|
||||
read_when:
|
||||
- Setting up BlueBubbles channel
|
||||
- Troubleshooting webhook pairing
|
||||
- Configuring iMessage on macOS
|
||||
title: "BlueBubbles"
|
||||
sidebarTitle: "BlueBubbles"
|
||||
---
|
||||
|
||||
Status: bundled legacy plugin that talks to the BlueBubbles macOS server over HTTP. Existing BlueBubbles setups continue to work, but new OpenClaw iMessage deployments should prefer the native [iMessage](/channels/imessage) plugin when its requirements fit your host.
|
||||
|
||||
<Warning>
|
||||
BlueBubbles is deprecated for new OpenClaw setups.
|
||||
|
||||
The upstream BlueBubbles ecosystem is still active, but OpenClaw depends on the BlueBubbles macOS server API. As of May 6, 2026, the official [`bluebubbles-server`](https://github.com/BlueBubblesApp/bluebubbles-server) development branch last changed on [January 22, 2026](https://github.com/BlueBubblesApp/bluebubbles-server/commit/88a4921bbd5a8111f1e9582b83715cf877171037), and the latest server release ([`v1.9.9`](https://github.com/BlueBubblesApp/bluebubbles-server/releases/tag/v1.9.9)) was published on May 16, 2025. The client app and helper repositories have newer activity, so this is not an abandonment claim; the deprecation is about reducing OpenClaw's dependency on an external HTTP server, webhooks, and private-API compatibility surface when the native `imsg` path keeps the integration on a local stdio contract.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
Current OpenClaw releases bundle BlueBubbles, so normal packaged builds do not need a separate `openclaw plugins install` step.
|
||||
</Note>
|
||||
|
||||
## Overview
|
||||
|
||||
- Runs on macOS via the BlueBubbles helper app ([bluebubbles.app](https://bluebubbles.app)).
|
||||
- Legacy fallback for installations that already rely on BlueBubbles channel IDs, webhook state, group targets, cron delivery, or workspace routing.
|
||||
- Recommended/tested: macOS Sequoia (15). macOS Tahoe (26) works; edit is currently broken on Tahoe, and group icon updates may report success but not sync.
|
||||
- OpenClaw talks to it through its REST API (`GET /api/v1/ping`, `POST /message/text`, `POST /chat/:id/*`).
|
||||
- Incoming messages arrive via webhooks; outgoing replies, typing indicators, read receipts, and tapbacks are REST calls.
|
||||
- Attachments and stickers are ingested as inbound media (and surfaced to the agent when possible).
|
||||
- Auto-TTS replies that synthesize MP3 or CAF audio are delivered as iMessage voice memo bubbles instead of plain file attachments.
|
||||
- Pairing/allowlist works the same way as other channels (`/channels/pairing` etc) with `channels.bluebubbles.allowFrom` + pairing codes.
|
||||
- Reactions are surfaced as system events just like Slack/Telegram so agents can "mention" them before replying.
|
||||
- Advanced features: edit, unsend, reply threading, message effects, group management.
|
||||
|
||||
## Quick start
|
||||
|
||||
<Steps>
|
||||
<Step title="Install BlueBubbles">
|
||||
Install the BlueBubbles server on your Mac (follow the instructions at [bluebubbles.app/install](https://bluebubbles.app/install)).
|
||||
</Step>
|
||||
<Step title="Enable the web API">
|
||||
In the BlueBubbles config, enable the web API and set a password.
|
||||
</Step>
|
||||
<Step title="Configure OpenClaw">
|
||||
Run `openclaw onboard` and select BlueBubbles, or configure manually:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enabled: true,
|
||||
serverUrl: "http://192.168.1.100:1234",
|
||||
password: "example-password",
|
||||
webhookPath: "/bluebubbles-webhook",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step title="Point webhooks at the gateway">
|
||||
Point BlueBubbles webhooks to your gateway (example: `https://your-gateway-host:3000/bluebubbles-webhook?password=<password>`).
|
||||
</Step>
|
||||
<Step title="Start the gateway">
|
||||
Start the gateway; it will register the webhook handler and start pairing.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Warning>
|
||||
**Security**
|
||||
|
||||
- Always set a webhook password.
|
||||
- Webhook authentication is always required. OpenClaw rejects BlueBubbles webhook requests unless they include a password/guid that matches `channels.bluebubbles.password` (for example `?password=<password>` or `x-password`), regardless of loopback/proxy topology.
|
||||
- Password authentication is checked before reading/parsing full webhook bodies.
|
||||
|
||||
</Warning>
|
||||
|
||||
## Keeping Messages.app alive (VM / headless setups)
|
||||
|
||||
Some macOS VM / always-on setups can end up with Messages.app going "idle" (incoming events stop until the app is opened/foregrounded). A simple workaround is to **poke Messages every 5 minutes** using an AppleScript + LaunchAgent.
|
||||
|
||||
<Steps>
|
||||
<Step title="Save the AppleScript">
|
||||
Save this as `~/Scripts/poke-messages.scpt`:
|
||||
|
||||
```applescript
|
||||
try
|
||||
tell application "Messages"
|
||||
if not running then
|
||||
launch
|
||||
end if
|
||||
|
||||
-- Touch the scripting interface to keep the process responsive.
|
||||
set _chatCount to (count of chats)
|
||||
end tell
|
||||
on error
|
||||
-- Ignore transient failures (first-run prompts, locked session, etc).
|
||||
end try
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step title="Install a LaunchAgent">
|
||||
Save this as `~/Library/LaunchAgents/com.user.poke-messages.plist`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.user.poke-messages</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>-lc</string>
|
||||
<string>/usr/bin/osascript "$HOME/Scripts/poke-messages.scpt"</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>StartInterval</key>
|
||||
<integer>300</integer>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/tmp/poke-messages.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/tmp/poke-messages.err</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
This runs **every 300 seconds** and **on login**. The first run may trigger macOS **Automation** prompts (`osascript` → Messages). Approve them in the same user session that runs the LaunchAgent.
|
||||
|
||||
</Step>
|
||||
<Step title="Load it">
|
||||
```bash
|
||||
launchctl unload ~/Library/LaunchAgents/com.user.poke-messages.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/com.user.poke-messages.plist
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Onboarding
|
||||
|
||||
BlueBubbles is available in interactive onboarding:
|
||||
|
||||
```
|
||||
openclaw onboard
|
||||
```
|
||||
|
||||
The wizard prompts for:
|
||||
|
||||
<ParamField path="Server URL" type="string" required>
|
||||
BlueBubbles server address (e.g., `http://192.168.1.100:1234`).
|
||||
</ParamField>
|
||||
<ParamField path="Password" type="string" required>
|
||||
API password from BlueBubbles Server settings.
|
||||
</ParamField>
|
||||
<ParamField path="Webhook path" type="string" default="/bluebubbles-webhook">
|
||||
Webhook endpoint path.
|
||||
</ParamField>
|
||||
<ParamField path="DM policy" type="string">
|
||||
`pairing`, `allowlist`, `open`, or `disabled`.
|
||||
</ParamField>
|
||||
<ParamField path="Allow list" type="string[]">
|
||||
Phone numbers, emails, or chat targets.
|
||||
</ParamField>
|
||||
|
||||
You can also add BlueBubbles via CLI:
|
||||
|
||||
```
|
||||
openclaw channels add bluebubbles --http-url http://192.168.1.100:1234 --password <password>
|
||||
```
|
||||
|
||||
## Access control (DMs + groups)
|
||||
|
||||
<Tabs>
|
||||
<Tab title="DMs">
|
||||
- Default: `channels.bluebubbles.dmPolicy = "pairing"`.
|
||||
- Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
|
||||
- Approve via:
|
||||
- `openclaw pairing list bluebubbles`
|
||||
- `openclaw pairing approve bluebubbles <CODE>`
|
||||
- Pairing is the default token exchange. Details: [Pairing](/channels/pairing)
|
||||
|
||||
</Tab>
|
||||
<Tab title="Groups">
|
||||
- `channels.bluebubbles.groupPolicy = open | allowlist | disabled` (default: `allowlist`).
|
||||
- `channels.bluebubbles.groupAllowFrom` controls who can trigger in groups when `allowlist` is set.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Contact name enrichment (macOS, optional)
|
||||
|
||||
BlueBubbles group webhooks often only include raw participant addresses. If you want `GroupMembers` context to show local contact names instead, you can opt in to local Contacts enrichment on macOS:
|
||||
|
||||
- `channels.bluebubbles.enrichGroupParticipantsFromContacts = true` enables the lookup. Default: `false`.
|
||||
- Lookups run only after group access, command authorization, and mention gating have allowed the message through.
|
||||
- Only unnamed phone participants are enriched.
|
||||
- Raw phone numbers remain as the fallback when no local match is found.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enrichGroupParticipantsFromContacts: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Mention gating (groups)
|
||||
|
||||
BlueBubbles supports mention gating for group chats, matching iMessage/WhatsApp behavior:
|
||||
|
||||
- Uses `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`) to detect mentions.
|
||||
- When `requireMention` is enabled for a group, the agent only responds when mentioned.
|
||||
- Control commands from authorized senders bypass mention gating.
|
||||
|
||||
Per-group configuration:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: {
|
||||
"*": { requireMention: true }, // default for all groups
|
||||
"iMessage;-;chat123": { requireMention: false }, // override for specific group
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Command gating
|
||||
|
||||
- Control commands (e.g., `/config`, `/model`) require authorization.
|
||||
- Uses `allowFrom` and `groupAllowFrom` to determine command authorization.
|
||||
- Authorized senders can run control commands even without mentioning in groups.
|
||||
|
||||
### Per-group system prompt
|
||||
|
||||
Each entry under `channels.bluebubbles.groups.*` accepts an optional `systemPrompt` string. The value is injected into the agent's system prompt on every turn that handles a message in that group, so you can set per-group persona or behavioral rules without editing agent prompts:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
groups: {
|
||||
"iMessage;-;chat123": {
|
||||
systemPrompt: "Keep responses under 3 sentences. Mirror the group's casual tone.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The key matches whatever BlueBubbles reports as `chatGuid` / `chatIdentifier` / numeric `chatId` for the group, and a `"*"` wildcard entry provides a default for every group without an exact match (same pattern used by `requireMention` and per-group tool policies). Exact matches always win over the wildcard. DMs ignore this field; use agent-level or account-level prompt customization instead.
|
||||
|
||||
#### Worked example: threaded replies and tapback reactions (Private API)
|
||||
|
||||
With the BlueBubbles Private API enabled, inbound messages arrive with short message IDs (for example `[[reply_to:5]]`) and the agent can call `action=reply` to thread into a specific message or `action=react` to drop a tapback. A per-group `systemPrompt` is a reliable way to keep the agent choosing the right tool:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
groups: {
|
||||
"iMessage;+;chat-family": {
|
||||
systemPrompt: "When replying in this group, always call action=reply with the [[reply_to:N]] messageId from context so your response threads under the triggering message. Never send a new unlinked message. For short acknowledgements ('ok', 'got it', 'on it'), use action=react with an appropriate tapback emoji (❤️, 👍, 😂, ‼️, ❓) instead of sending a text reply.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Tapback reactions and threaded replies both require the BlueBubbles Private API; see [Advanced actions](#advanced-actions) and [Message IDs](#message-ids-short-vs-full) for the underlying mechanics.
|
||||
|
||||
## ACP conversation bindings
|
||||
|
||||
BlueBubbles chats can be turned into durable ACP workspaces without changing the transport layer.
|
||||
|
||||
Fast operator flow:
|
||||
|
||||
- Run `/acp spawn codex --bind here` inside the DM or allowed group chat.
|
||||
- Future messages in that same BlueBubbles conversation route to the spawned ACP session.
|
||||
- `/new` and `/reset` reset the same bound ACP session in place.
|
||||
- `/acp close` closes the ACP session and removes the binding.
|
||||
|
||||
Configured persistent bindings are also supported through top-level `bindings[]` entries with `type: "acp"` and `match.channel: "bluebubbles"`.
|
||||
|
||||
`match.peer.id` can use any supported BlueBubbles target form:
|
||||
|
||||
- normalized DM handle such as `+15555550123` or `user@example.com`
|
||||
- `chat_id:<id>`
|
||||
- `chat_guid:<guid>`
|
||||
- `chat_identifier:<identifier>`
|
||||
|
||||
For stable group bindings, prefer `chat_id:*` or `chat_identifier:*`.
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "codex",
|
||||
runtime: {
|
||||
type: "acp",
|
||||
acp: { agent: "codex", backend: "acpx", mode: "persistent" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
type: "acp",
|
||||
agentId: "codex",
|
||||
match: {
|
||||
channel: "bluebubbles",
|
||||
accountId: "default",
|
||||
peer: { kind: "dm", id: "+15555550123" },
|
||||
},
|
||||
acp: { label: "codex-imessage" },
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
|
||||
|
||||
## Typing + read receipts
|
||||
|
||||
- **Typing indicators**: Sent automatically before and during response generation.
|
||||
- **Read receipts**: Controlled by `channels.bluebubbles.sendReadReceipts` (default: `true`).
|
||||
- **Typing indicators**: OpenClaw sends typing start events; BlueBubbles clears typing automatically on send or timeout (manual stop via DELETE is unreliable).
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
sendReadReceipts: false, // disable read receipts
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced actions
|
||||
|
||||
BlueBubbles supports advanced message actions when enabled in config:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
actions: {
|
||||
reactions: true, // tapbacks (default: true)
|
||||
edit: true, // edit sent messages (macOS 13+, broken on macOS 26 Tahoe)
|
||||
unsend: true, // unsend messages (macOS 13+)
|
||||
reply: true, // reply threading by message GUID
|
||||
sendWithEffect: true, // message effects (slam, loud, etc.)
|
||||
renameGroup: true, // rename group chats
|
||||
setGroupIcon: true, // set group chat icon/photo (flaky on macOS 26 Tahoe)
|
||||
addParticipant: true, // add participants to groups
|
||||
removeParticipant: true, // remove participants from groups
|
||||
leaveGroup: true, // leave group chats
|
||||
sendAttachment: true, // send attachments/media
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Available actions">
|
||||
- **react**: Add/remove tapback reactions (`messageId`, `emoji`, `remove`). iMessage's native tapback set is `love`, `like`, `dislike`, `laugh`, `emphasize`, and `question`. When an agent picks an emoji outside that set (for example `👀`), the reaction tool falls back to `love` so the tapback still renders instead of failing the whole request. Configured ack reactions still validate strictly and error on unknown values.
|
||||
- **edit**: Edit a sent message (`messageId`, `text`).
|
||||
- **unsend**: Unsend a message (`messageId`).
|
||||
- **reply**: Reply to a specific message (`messageId`, `text`, `to`).
|
||||
- **sendWithEffect**: Send with iMessage effect (`text`, `to`, `effectId`).
|
||||
- **renameGroup**: Rename a group chat (`chatGuid`, `displayName`).
|
||||
- **setGroupIcon**: Set a group chat's icon/photo (`chatGuid`, `media`) - flaky on macOS 26 Tahoe (API may return success but the icon does not sync).
|
||||
- **addParticipant**: Add someone to a group (`chatGuid`, `address`).
|
||||
- **removeParticipant**: Remove someone from a group (`chatGuid`, `address`).
|
||||
- **leaveGroup**: Leave a group chat (`chatGuid`).
|
||||
- **upload-file**: Send media/files (`to`, `buffer`, `filename`, `asVoice`).
|
||||
- Voice memos: set `asVoice: true` with **MP3** or **CAF** audio to send as an iMessage voice message. BlueBubbles converts MP3 → CAF when sending voice memos.
|
||||
- Legacy alias: `sendAttachment` still works, but `upload-file` is the canonical action name.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
### Message IDs (short vs full)
|
||||
|
||||
OpenClaw may surface _short_ message IDs (e.g., `1`, `2`) to save tokens.
|
||||
|
||||
- `MessageSid` / `ReplyToId` can be short IDs.
|
||||
- `MessageSidFull` / `ReplyToIdFull` contain the provider full IDs.
|
||||
- Short IDs are in-memory; they can expire on restart or cache eviction.
|
||||
- Actions accept short or full `messageId`, but short IDs will error if no longer available.
|
||||
|
||||
Use full IDs for durable automations and storage:
|
||||
|
||||
- Templates: `{{MessageSidFull}}`, `{{ReplyToIdFull}}`
|
||||
- Context: `MessageSidFull` / `ReplyToIdFull` in inbound payloads
|
||||
|
||||
See [Configuration](/gateway/configuration) for template variables.
|
||||
|
||||
<a id="coalescing-split-send-dms-command--url-in-one-composition"></a>
|
||||
|
||||
## Coalescing split-send DMs (command + URL in one composition)
|
||||
|
||||
When a user types a command and a URL together in iMessage - e.g. `Dump https://example.com/article` - Apple splits the send into **two separate webhook deliveries**:
|
||||
|
||||
1. A text message (`"Dump"`).
|
||||
2. A URL-preview balloon (`"https://..."`) with OG-preview images as attachments.
|
||||
|
||||
The two webhooks arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalescing, the agent receives the command alone on turn 1, replies (often "send me the URL"), and only sees the URL on turn 2 - at which point the command context is already lost.
|
||||
|
||||
`channels.bluebubbles.coalesceSameSenderDms` opts a DM into merging consecutive same-sender webhooks into a single agent turn. Group chats continue to key per-message so multi-user turn structure is preserved.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="When to enable">
|
||||
Enable when:
|
||||
|
||||
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
|
||||
- Your users paste URLs, images, or long content alongside commands.
|
||||
- You can accept the added DM turn latency (see below).
|
||||
|
||||
Leave disabled when:
|
||||
|
||||
- You need minimum command latency for single-word DM triggers.
|
||||
- All your flows are one-shot commands without payload follow-ups.
|
||||
|
||||
</Tab>
|
||||
<Tab title="Enabling">
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
coalesceSameSenderDms: true, // opt in (default: false)
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
With the flag on and no explicit `messages.inbound.byChannel.bluebubbles`, the debounce window widens to **2500 ms** (the default for non-coalescing is 500 ms). The wider window is required - Apple's split-send cadence of 0.8-2.0 s does not fit in the tighter default.
|
||||
|
||||
To tune the window yourself:
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
inbound: {
|
||||
byChannel: {
|
||||
// 2500 ms works for most setups; raise to 4000 ms if your Mac is slow
|
||||
// or under memory pressure (observed gap can stretch past 2 s then).
|
||||
bluebubbles: 2500,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Trade-offs">
|
||||
- **Added latency for DM control commands.** With the flag on, DM control-command messages (like `Dump`, `Save`, etc.) now wait up to the debounce window before dispatching, in case a payload webhook is coming. Group-chat commands keep instant dispatch.
|
||||
- **Merged output is bounded** - merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source `messageId` still reaches inbound-dedupe so a later MessagePoller replay of any individual event is recognized as a duplicate.
|
||||
- **Opt-in, per-channel.** Other channels (Telegram, WhatsApp, Slack, …) are unaffected.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Scenarios and what the agent sees
|
||||
|
||||
| User composes | Apple delivers | Flag off (default) | Flag on + 2500 ms window |
|
||||
| ------------------------------------------------------------------ | ------------------------- | --------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `Dump https://example.com` (one send) | 2 webhooks ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text `Dump https://example.com` |
|
||||
| `Save this 📎image.jpg caption` (attachment + text) | 2 webhooks | Two turns | One turn: text + image |
|
||||
| `/status` (standalone command) | 1 webhook | Instant dispatch | **Wait up to window, then dispatch** |
|
||||
| URL pasted alone | 1 webhook | Instant dispatch | Instant dispatch (only one entry in bucket) |
|
||||
| Text + URL sent as two deliberate separate messages, minutes apart | 2 webhooks outside window | Two turns | Two turns (window expires between them) |
|
||||
| Rapid flood (>10 small DMs inside window) | N webhooks | N turns | One turn, bounded output (first + latest, text/attachment caps applied) |
|
||||
|
||||
### Split-send coalescing troubleshooting
|
||||
|
||||
If the flag is on and split-sends still arrive as two turns, check each layer:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Config actually loaded">
|
||||
```
|
||||
grep coalesceSameSenderDms ~/.openclaw/openclaw.json
|
||||
```
|
||||
|
||||
Then `openclaw gateway restart` - the flag is read at debouncer-registry creation.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Debounce window wide enough for your setup">
|
||||
Look at the BlueBubbles server log under `~/Library/Logs/bluebubbles-server/main.log`:
|
||||
|
||||
```
|
||||
grep -E "Dispatching event to webhook" main.log | tail -20
|
||||
```
|
||||
|
||||
Measure the gap between the `"Dump"`-style text dispatch and the `"https://..."; Attachments:` dispatch that follows. Raise `messages.inbound.byChannel.bluebubbles` to comfortably cover that gap.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Session JSONL timestamps ≠ webhook arrival">
|
||||
Session event timestamps (`~/.openclaw/agents/<id>/sessions/*.jsonl`) reflect when the gateway hands a message to the agent, **not** when the webhook arrived. A queued-second message tagged `[Queued messages while agent was busy]` means the first turn was still running when the second webhook arrived - the coalesce bucket had already flushed. Tune the window against the BB server log, not the session log.
|
||||
</Accordion>
|
||||
<Accordion title="Memory pressure slowing reply dispatch">
|
||||
On smaller machines (8 GB), agent turns can take long enough that the coalesce bucket flushes before the reply completes, and the URL lands as a queued second turn. Check `memory_pressure` and `ps -o rss -p $(pgrep openclaw-gateway)`; if the gateway is over ~500 MB RSS and the compressor is active, close other heavy processes or bump to a larger host.
|
||||
</Accordion>
|
||||
<Accordion title="Reply-quote sends are a different path">
|
||||
If the user tapped `Dump` as a **reply** to an existing URL-balloon (iMessage shows a "1 Reply" badge on the Dump bubble), the URL lives in `replyToBody`, not in a second webhook. Coalescing does not apply - that's a skill/prompt concern, not a debouncer concern.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Block streaming
|
||||
|
||||
Control whether responses are sent as a single message or streamed in blocks:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
blockStreaming: true, // enable block streaming (off by default)
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Media + limits
|
||||
|
||||
- Inbound attachments are downloaded and stored in the media cache.
|
||||
- Media cap via `channels.bluebubbles.mediaMaxMb` for inbound and outbound media (default: 8 MB).
|
||||
- Outbound text is chunked to `channels.bluebubbles.textChunkLimit` (default: 4000 chars).
|
||||
|
||||
## Configuration reference
|
||||
|
||||
Full configuration: [Configuration](/gateway/configuration)
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Connection and webhook">
|
||||
- `channels.bluebubbles.enabled`: Enable/disable the channel.
|
||||
- `channels.bluebubbles.serverUrl`: BlueBubbles REST API base URL.
|
||||
- `channels.bluebubbles.password`: API password.
|
||||
- `channels.bluebubbles.webhookPath`: Webhook endpoint path (default: `/bluebubbles-webhook`).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Access policy">
|
||||
- `channels.bluebubbles.dmPolicy`: `pairing | allowlist | open | disabled` (default: `pairing`).
|
||||
- `channels.bluebubbles.allowFrom`: DM allowlist (handles, emails, E.164 numbers, `chat_id:*`, `chat_guid:*`).
|
||||
- `channels.bluebubbles.groupPolicy`: `open | allowlist | disabled` (default: `allowlist`).
|
||||
- `channels.bluebubbles.groupAllowFrom`: Group sender allowlist.
|
||||
- `channels.bluebubbles.enrichGroupParticipantsFromContacts`: On macOS, optionally enrich unnamed group participants from local Contacts after gating passes. Default: `false`.
|
||||
- `channels.bluebubbles.groups`: Per-group config (`requireMention`, etc.).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Delivery and chunking">
|
||||
- `channels.bluebubbles.sendReadReceipts`: Send read receipts (default: `true`).
|
||||
- `channels.bluebubbles.blockStreaming`: Enable block streaming (default: `false`; required for streaming replies).
|
||||
- `channels.bluebubbles.textChunkLimit`: Outbound chunk size in chars (default: 4000).
|
||||
- `channels.bluebubbles.sendTimeoutMs`: Per-request timeout in ms for outbound text sends via `/api/v1/message/text` (default: 30000). Raise on macOS 26 setups where Private API iMessage sends can stall for 60+ seconds inside the iMessage framework; for example `45000` or `60000`. Probes, chat lookups, reactions, edits, and health checks currently keep the shorter 10s default; broadening coverage to reactions and edits is planned as a follow-up. Per-account override: `channels.bluebubbles.accounts.<accountId>.sendTimeoutMs`.
|
||||
- `channels.bluebubbles.chunkMode`: `length` (default) splits only when exceeding `textChunkLimit`; `newline` splits on blank lines (paragraph boundaries) before length chunking.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Media and history">
|
||||
- `channels.bluebubbles.mediaMaxMb`: Inbound/outbound media cap in MB (default: 8).
|
||||
- `channels.bluebubbles.mediaLocalRoots`: Explicit allowlist of absolute local directories permitted for outbound local media paths. Local path sends are denied by default unless this is configured. Per-account override: `channels.bluebubbles.accounts.<accountId>.mediaLocalRoots`.
|
||||
- `channels.bluebubbles.coalesceSameSenderDms`: Merge consecutive same-sender DM webhooks into one agent turn so Apple's text+URL split-send arrives as a single message (default: `false`). See [Coalescing split-send DMs](#coalescing-split-send-dms-command--url-in-one-composition) for scenarios, window tuning, and trade-offs. Widens the default inbound debounce window from 500 ms to 2500 ms when enabled without an explicit `messages.inbound.byChannel.bluebubbles`.
|
||||
- `channels.bluebubbles.historyLimit`: Max group messages for context (0 disables).
|
||||
- `channels.bluebubbles.dmHistoryLimit`: DM history limit.
|
||||
- `channels.bluebubbles.replyContextApiFallback`: When an inbound reply lands without `replyToBody`/`replyToSender` and the in-memory reply-context cache misses, fetch the original message from the BlueBubbles HTTP API as a best-effort fallback (default: `false`). Useful for multi-instance deployments sharing one BlueBubbles account, after process restarts, or after long-lived TTL/LRU cache eviction. The fetch is SSRF-guarded by the same policy as every other BlueBubbles client request, never throws, and populates the cache so subsequent replies amortize. Per-account override: `channels.bluebubbles.accounts.<accountId>.replyContextApiFallback`. A channel-level setting propagates to accounts that omit the flag.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Actions and accounts">
|
||||
- `channels.bluebubbles.actions`: Enable/disable specific actions.
|
||||
- `channels.bluebubbles.accounts`: Multi-account configuration.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
Related global options:
|
||||
|
||||
- `agents.list[].groupChat.mentionPatterns` (or `messages.groupChat.mentionPatterns`).
|
||||
- `messages.responsePrefix`.
|
||||
|
||||
## Addressing / delivery targets
|
||||
|
||||
Prefer `chat_guid` for stable routing:
|
||||
|
||||
- `chat_guid:iMessage;-;+15555550123` (preferred for groups)
|
||||
- `chat_id:123`
|
||||
- `chat_identifier:...`
|
||||
- Direct handles: `+15555550123`, `user@example.com`
|
||||
- If a direct handle does not have an existing DM chat, OpenClaw will create one via `POST /api/v1/chat/new`. This requires the BlueBubbles Private API to be enabled.
|
||||
|
||||
### iMessage vs SMS routing
|
||||
|
||||
When the same handle has both an iMessage and an SMS chat on the Mac (for example a phone number that is iMessage-registered but has also received green-bubble fallbacks), OpenClaw prefers the iMessage chat and never silently downgrades to SMS. To force the SMS chat, use an explicit `sms:` target prefix (for example `sms:+15555550123`). Handles without a matching iMessage chat still send through whatever chat BlueBubbles reports.
|
||||
|
||||
## Security
|
||||
|
||||
- Webhook requests are authenticated by comparing `guid`/`password` query params or headers against `channels.bluebubbles.password`.
|
||||
- Keep the API password and webhook endpoint secret (treat them like credentials).
|
||||
- There is no localhost bypass for BlueBubbles webhook auth. If you proxy webhook traffic, keep the BlueBubbles password on the request end-to-end. `gateway.trustedProxies` does not replace `channels.bluebubbles.password` here. See [Gateway security](/gateway/security#reverse-proxy-configuration).
|
||||
- Enable HTTPS + firewall rules on the BlueBubbles server if exposing it outside your LAN.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- If typing/read events stop working, check the BlueBubbles webhook logs and verify the gateway path matches `channels.bluebubbles.webhookPath`.
|
||||
- Pairing codes expire after one hour; use `openclaw pairing list bluebubbles` and `openclaw pairing approve bluebubbles <code>`.
|
||||
- Reactions require the BlueBubbles private API (`POST /api/v1/message/react`); ensure the server version exposes it.
|
||||
- Edit/unsend require macOS 13+ and a compatible BlueBubbles server version. On macOS 26 (Tahoe), edit is currently broken due to private API changes.
|
||||
- Group icon updates can be flaky on macOS 26 (Tahoe): the API may return success but the new icon does not sync.
|
||||
- OpenClaw auto-hides known-broken actions based on the BlueBubbles server's macOS version. If edit still appears on macOS 26 (Tahoe), disable it manually with `channels.bluebubbles.actions.edit=false`.
|
||||
- `coalesceSameSenderDms` enabled but split-sends (e.g. `Dump` + URL) still arrive as two turns: see the [split-send coalescing troubleshooting](#split-send-coalescing-troubleshooting) checklist - common causes are too-tight debounce window, session-log timestamps misread as webhook arrival, or a reply-quote send (which uses `replyToBody`, not a second webhook).
|
||||
- For status/health info: `openclaw status --all` or `openclaw status --deep`.
|
||||
|
||||
For general channel workflow reference, see [Channels](/channels) and the [Plugins](/tools/plugin) guide.
|
||||
|
||||
## Related
|
||||
|
||||
- [Channel Routing](/channels/channel-routing) - session routing for messages
|
||||
- [Channels Overview](/channels) - all supported channels
|
||||
- [Groups](/channels/groups) - group chat behavior and mention gating
|
||||
- [Pairing](/channels/pairing) - DM authentication and pairing flow
|
||||
- [Security](/gateway/security) - access model and hardening
|
||||
@@ -662,7 +662,7 @@ Default slash command settings:
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Live stream preview">
|
||||
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming` takes `off` | `partial` | `block` | `progress` (default). `progress` keeps one editable status draft and updates it with tool progress until final delivery; `streamMode` is a legacy runtime alias. Run `openclaw doctor --fix` to rewrite persisted config to the canonical key.
|
||||
OpenClaw can stream draft replies by sending a temporary message and editing it as text arrives. `channels.discord.streaming` takes `off` | `partial` | `block` | `progress` (default). `progress` keeps one editable status draft and updates it with tool progress until final delivery; the shared starter label is a rolling line, so it scrolls away like the rest once enough work appears. `streamMode` is a legacy runtime alias. Run `openclaw doctor --fix` to rewrite persisted config to the canonical key.
|
||||
|
||||
Set `channels.discord.streaming.mode` to `off` to disable Discord preview edits. If Discord block streaming is explicitly enabled, OpenClaw skips the preview stream to avoid double-streaming.
|
||||
|
||||
@@ -687,6 +687,7 @@ Default slash command settings:
|
||||
- `block` emits draft-sized chunks (use `draftChunk` to tune size and breakpoints, clamped to `textChunkLimit`).
|
||||
- Media, error, and explicit-reply finals cancel pending preview edits.
|
||||
- `streaming.preview.toolProgress` (default `true`) controls whether tool/progress updates reuse the preview message.
|
||||
- Tool/progress rows render as compact emoji + title + detail when available, for example `🛠️ Bash: run tests` or `🔎 Web Search: for "query"`.
|
||||
- `streaming.preview.commandText` / `streaming.progress.commandText` controls command/exec detail in compact progress lines: `raw` (default) or `status` (tool label only).
|
||||
|
||||
Hide raw command/exec text while keeping compact progress lines:
|
||||
@@ -1184,7 +1185,10 @@ Auto-join example:
|
||||
reconnectGraceMs: 15000,
|
||||
tts: {
|
||||
provider: "openai",
|
||||
openai: { voice: "onyx" },
|
||||
openai: {
|
||||
model: "gpt-4o-mini-tts",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1195,8 +1199,9 @@ Auto-join example:
|
||||
Notes:
|
||||
|
||||
- `voice.tts` overrides `messages.tts` for voice playback only.
|
||||
- `voice.model` overrides the LLM used for Discord voice channel responses only. Leave it unset to inherit the routed agent model.
|
||||
- `voice.model` overrides the LLM used for Discord voice channel responses only. Leave it unset to inherit the routed agent model. Do not set this to `gpt-realtime-2`; Discord voice channels use STT plus TTS playback, not the OpenAI Realtime session transport.
|
||||
- STT uses `tools.media.audio`; `voice.model` does not affect transcription.
|
||||
- For an OpenAI voice on Discord playback, set `voice.tts.provider: "openai"` and choose a Text-to-speech voice under `voice.tts.openai.voice` or `voice.tts.providers.openai.voice`. `cedar` is a good masculine-sounding choice on the current OpenAI TTS model.
|
||||
- Per-channel Discord `systemPrompt` overrides apply to voice transcript turns for that voice channel.
|
||||
- Voice transcript turns derive owner status from Discord `allowFrom` (or `dm.allowFrom`); non-owner speakers cannot access owner-only tools (for example `gateway` and `cron`).
|
||||
- Discord voice is opt-in for text-only configs; set `channels.discord.voice.enabled=true` (or keep an existing `channels.discord.voice` block) to enable `/vc` commands, the voice runtime, and the `GuildVoiceStates` gateway intent.
|
||||
@@ -1206,9 +1211,12 @@ Notes:
|
||||
- `voice.connectTimeoutMs` controls the initial `@discordjs/voice` Ready wait for `/vc join` and auto-join attempts. Default: `30000`.
|
||||
- `voice.reconnectGraceMs` controls how long OpenClaw waits for a disconnected voice session to begin reconnecting before destroying it. Default: `15000`.
|
||||
- Voice playback does not stop just because another user starts speaking. To avoid feedback loops, OpenClaw ignores new voice capture while TTS is playing; speak after playback finishes for the next turn.
|
||||
- `voice.captureSilenceGraceMs` controls how long OpenClaw waits after Discord reports a speaker has stopped before finalizing that audio segment for STT. Default: `2500`; raise this if Discord splits normal pauses into choppy partial transcripts.
|
||||
- When ElevenLabs is the selected TTS provider, Discord voice playback uses streaming TTS and starts from the provider response stream. Providers without streaming support fall back to the synthesized temp-file path.
|
||||
- OpenClaw also watches receive decrypt failures and auto-recovers by leaving/rejoining the voice channel after repeated failures in a short window.
|
||||
- If receive logs repeatedly show `DecryptionFailed(UnencryptedWhenPassthroughDisabled)` after updating, collect a dependency report and logs. The bundled `@discordjs/voice` line includes the upstream padding fix from discord.js PR #11449, which closed discord.js issue #11419.
|
||||
- `The operation was aborted` receive events are expected when OpenClaw finalizes a captured speaker segment; they are verbose diagnostics, not warnings.
|
||||
- Verbose Discord voice logs include a bounded one-line STT transcript preview for each accepted speaker segment, so debugging shows both the user side and the agent reply side without dumping unbounded transcript text.
|
||||
|
||||
Voice channel pipeline:
|
||||
|
||||
@@ -1216,7 +1224,7 @@ Voice channel pipeline:
|
||||
- `tools.media.audio` handles STT, for example `openai/gpt-4o-mini-transcribe`.
|
||||
- The transcript is sent through Discord ingress and routing while the response LLM runs with a voice-output policy that hides the agent `tts` tool and asks for returned text, because Discord voice owns final TTS playback.
|
||||
- `voice.model`, when set, overrides only the response LLM for this voice-channel turn.
|
||||
- `voice.tts` is merged over `messages.tts`; the resulting audio is played in the joined channel.
|
||||
- `voice.tts` is merged over `messages.tts`; streaming-capable providers feed the player directly, otherwise the resulting audio file is played in the joined channel.
|
||||
|
||||
Credentials are resolved per component: LLM route auth for `voice.model`, STT auth for `tools.media.audio`, and TTS auth for `messages.tts`/`voice.tts`.
|
||||
|
||||
|
||||
@@ -482,10 +482,6 @@ Group inbound payloads set:
|
||||
- `WasMentioned` (mention gating result)
|
||||
- Telegram forum topics also include `MessageThreadId` and `IsForum`.
|
||||
|
||||
Channel-specific notes:
|
||||
|
||||
- BlueBubbles can optionally enrich unnamed macOS group participants from the local Contacts database before populating `GroupMembers`. This is off by default and only runs after normal group gating passes.
|
||||
|
||||
The agent system prompt includes a group intro on the first turn of a new group session. It reminds the model to respond like a human, avoid Markdown tables, minimize empty lines and follow normal chat spacing, and avoid typing literal `\n` sequences. Channel-sourced group names and participant labels are rendered as fenced untrusted metadata, not inline system instructions.
|
||||
|
||||
## iMessage specifics
|
||||
|
||||
226
docs/channels/imessage-from-bluebubbles.md
Normal file
226
docs/channels/imessage-from-bluebubbles.md
Normal file
@@ -0,0 +1,226 @@
|
||||
---
|
||||
summary: "Switch from the BlueBubbles plugin to the bundled iMessage plugin without losing pairing, allowlists, or group bindings."
|
||||
read_when:
|
||||
- Planning a move from BlueBubbles to the bundled iMessage plugin
|
||||
- Translating BlueBubbles config keys to iMessage equivalents
|
||||
- Rolling back a partial iMessage cutover
|
||||
title: "Coming from BlueBubbles"
|
||||
---
|
||||
|
||||
The bundled `imessage` plugin now reaches the same private API surface as BlueBubbles (`react`, `edit`, `unsend`, `reply`, `sendWithEffect`, group management, attachments) by driving [`steipete/imsg`](https://github.com/steipete/imsg) over JSON-RPC. If you already run a Mac with `imsg` installed, you can drop the BlueBubbles server and let the plugin talk to Messages.app directly.
|
||||
|
||||
This guide is opt-in. BlueBubbles still works and remains the right choice if you cannot run `imsg` on the host where the Mac signs into iMessage (for example, if the Mac is unreachable from the gateway).
|
||||
|
||||
## When this migration makes sense
|
||||
|
||||
- You already run `imsg` on the same Mac (or one reachable over SSH) where Messages.app is signed in.
|
||||
- You want one fewer moving part — no separate BlueBubbles server, no REST endpoint to authenticate, no webhook plumbing. Single CLI binary instead of a server + client app + helper.
|
||||
- You are on a [supported macOS / `imsg` build](/channels/imessage#requirements-and-permissions-macos) where the private API probe reports `available: true`.
|
||||
|
||||
## When to stay on BlueBubbles
|
||||
|
||||
- The Mac with Messages.app is on a network the gateway cannot reach via SSH.
|
||||
- You depend on BlueBubbles features the bundled plugin does not yet cover (rich text formatting attributes beyond bold/italic/underline/strikethrough, BlueBubbles-specific webhook integrations).
|
||||
- Your current setup hard-codes BlueBubbles webhook URLs into other systems that you cannot rewire.
|
||||
|
||||
## Before you start
|
||||
|
||||
1. Install `imsg` on the Mac that runs Messages.app:
|
||||
|
||||
```bash
|
||||
brew install steipete/tap/imsg
|
||||
imsg launch
|
||||
imsg rpc --help
|
||||
```
|
||||
|
||||
2. Verify the private API bridge:
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
You want `imessage.privateApi.available: true`. If it reports `false`, fix that first — see [Capability detection](/channels/imessage#private-api-actions).
|
||||
|
||||
3. Snapshot your config so you can roll back:
|
||||
|
||||
```bash
|
||||
cp ~/.openclaw/openclaw.json5 ~/.openclaw/openclaw.json5.bak
|
||||
```
|
||||
|
||||
## Config translation
|
||||
|
||||
iMessage and BlueBubbles share a lot of channel-level config. The keys that change are mostly transport (REST server vs local CLI). Behavior keys (`dmPolicy`, `groupPolicy`, `allowFrom`, etc.) keep the same meaning.
|
||||
|
||||
| BlueBubbles | bundled iMessage | Notes |
|
||||
| ---------------------------------------------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `channels.bluebubbles.enabled` | `channels.imessage.enabled` | Same semantics. |
|
||||
| `channels.bluebubbles.serverUrl` | _(removed)_ | No REST server — the plugin spawns `imsg rpc` over stdio. |
|
||||
| `channels.bluebubbles.password` | _(removed)_ | No webhook authentication needed. |
|
||||
| _(implicit)_ | `channels.imessage.cliPath` | Path to `imsg` (default `imsg`); use a wrapper script for SSH. |
|
||||
| _(implicit)_ | `channels.imessage.dbPath` | Optional Messages.app `chat.db` override; auto-detected when omitted. |
|
||||
| _(implicit)_ | `channels.imessage.remoteHost` | `host` or `user@host` — only needed when `cliPath` is an SSH wrapper and you want SCP attachment fetches. |
|
||||
| `channels.bluebubbles.dmPolicy` | `channels.imessage.dmPolicy` | Same values (`pairing` / `allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.allowFrom` | `channels.imessage.allowFrom` | Pairing approvals carry over by handle, not by token. |
|
||||
| `channels.bluebubbles.groupPolicy` | `channels.imessage.groupPolicy` | Same values (`allowlist` / `open` / `disabled`). |
|
||||
| `channels.bluebubbles.groupAllowFrom` | `channels.imessage.groupAllowFrom` | Same. |
|
||||
| `channels.bluebubbles.groups` | `channels.imessage.groups` | **Copy this verbatim, including any `groups: { "*": { ... } }` wildcard entry.** Per-group `requireMention`, `tools`, `toolsBySender` carry over. With `groupPolicy: "allowlist"`, an empty or missing `groups` block silently drops every group message — see "Group registry footgun" below. |
|
||||
| `channels.bluebubbles.sendReadReceipts` | `channels.imessage.sendReadReceipts` | Default `true`. With the bundled plugin this only fires when the private API probe is up. |
|
||||
| `channels.bluebubbles.includeAttachments` | `channels.imessage.includeAttachments` | Same. |
|
||||
| `channels.bluebubbles.attachmentRoots` | `channels.imessage.attachmentRoots` | Local roots; same wildcard rules. |
|
||||
| _(N/A)_ | `channels.imessage.remoteAttachmentRoots` | Only used when `remoteHost` is set for SCP fetches. |
|
||||
| `channels.bluebubbles.mediaMaxMb` | `channels.imessage.mediaMaxMb` | Default 16 MB on iMessage (BlueBubbles default was 8 MB). Set explicitly if you want to keep the lower cap. |
|
||||
| `channels.bluebubbles.textChunkLimit` | `channels.imessage.textChunkLimit` | Default 4000 on both. |
|
||||
| `channels.bluebubbles.coalesceSameSenderDms` | `channels.imessage.coalesceSameSenderDms` | Same opt-in. DM-only — group chats keep instant per-message dispatch on both channels. Widens the default inbound debounce to 2500 ms when enabled without an explicit `messages.inbound.byChannel.imessage`. See [iMessage docs § Coalescing split-send DMs](/channels/imessage#coalescing-split-send-dms-command--url-in-one-composition). |
|
||||
| `channels.bluebubbles.enrichGroupParticipantsFromContacts` | _(N/A)_ | iMessage already reads sender display names from `chat.db`. |
|
||||
| `channels.bluebubbles.actions.*` | `channels.imessage.actions.*` | Per-action toggles: `reactions`, `edit`, `unsend`, `reply`, `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, `sendAttachment`. |
|
||||
|
||||
Multi-account configs (`channels.bluebubbles.accounts.*`) translate one-to-one to `channels.imessage.accounts.*`.
|
||||
|
||||
## Group registry footgun
|
||||
|
||||
The bundled iMessage plugin runs **two** separate group allowlist gates back-to-back. Both must pass for a group message to reach the agent:
|
||||
|
||||
1. **Sender / chat-target allowlist** (`channels.imessage.groupAllowFrom`) — checked by `isAllowedIMessageSender`. Matches inbound messages by sender handle, `chat_guid`, `chat_identifier`, or `chat_id`. Same shape as BlueBubbles.
|
||||
2. **Group registry** (`channels.imessage.groups`) — checked by `resolveChannelGroupPolicy` from `inbound-processing.ts:199`. With `groupPolicy: "allowlist"`, this gate requires either:
|
||||
- a `groups: { "*": { ... } }` wildcard entry (sets `allowAll = true`), or
|
||||
- an explicit per-`chat_id` entry under `groups`.
|
||||
|
||||
If gate 1 passes but gate 2 fails, the message is dropped. The plugin emits two `warn`-level signals so this is no longer silent at default log level:
|
||||
|
||||
- A one-time startup `warn` per account when `groupPolicy: "allowlist"` is set but `channels.imessage.groups` is empty (no `"*"` wildcard, no per-`chat_id` entries) — fired before any messages land.
|
||||
- A one-time per-`chat_id` `warn` the first time a specific group is dropped at runtime, naming the chat_id and the exact key to add to `groups` to allow it.
|
||||
|
||||
DMs continue to work because they take a different code path.
|
||||
|
||||
This is the most common BlueBubbles → bundled-iMessage migration failure mode: operators copy `groupAllowFrom` and `groupPolicy` but skip the `groups` block, because BlueBubbles' `groups: { "*": { "requireMention": true } }` looks like an unrelated mention setting. It's actually load-bearing for the registry gate.
|
||||
|
||||
The minimum config to keep group messages flowing after `groupPolicy: "allowlist"`:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123", "chat_guid:any;-;..."],
|
||||
groups: {
|
||||
"*": { requireMention: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`requireMention: true` under `*` is harmless when no mention patterns are configured: the runtime sets `canDetectMention = false` and short-circuits the mention drop at `inbound-processing.ts:512`. With mention patterns configured (`agents.list[].groupChat.mentionPatterns`), it works as expected.
|
||||
|
||||
If the gateway logs `imessage: dropping group message from chat_id=<id>` or the startup line `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty`, gate 2 is dropping — add the `groups` block.
|
||||
|
||||
## Step-by-step
|
||||
|
||||
1. Add an iMessage block alongside the existing BlueBubbles block. Do not delete BlueBubbles yet:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enabled: true,
|
||||
// ... existing config ...
|
||||
},
|
||||
imessage: {
|
||||
enabled: false, // turn on after the dry run below
|
||||
cliPath: "/opt/homebrew/bin/imsg",
|
||||
dmPolicy: "pairing",
|
||||
allowFrom: ["+15555550123"], // copy from bluebubbles.allowFrom
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: [], // copy from bluebubbles.groupAllowFrom
|
||||
groups: { "*": { requireMention: true } }, // copy from bluebubbles.groups — silently drops groups if missing, see "Group registry footgun" above
|
||||
actions: {
|
||||
reactions: true,
|
||||
edit: true,
|
||||
unsend: true,
|
||||
reply: true,
|
||||
sendWithEffect: true,
|
||||
sendAttachment: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
2. **Dry-run probe** — start the gateway and confirm both channels report healthy:
|
||||
|
||||
```bash
|
||||
openclaw gateway
|
||||
openclaw channels status
|
||||
openclaw channels status --probe # expect imessage.privateApi.available: true
|
||||
```
|
||||
|
||||
Because `imessage.enabled` is still `false`, no inbound iMessage traffic is routed yet — but `--probe` exercises the bridge so you catch permission/install issues before the cutover.
|
||||
|
||||
3. **Cut over.** Disable BlueBubbles and enable iMessage in one config edit:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: { enabled: false }, // keep the rest of the block for rollback
|
||||
imessage: { enabled: true /* ... */ },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Restart the gateway. Inbound iMessage traffic now flows through the bundled plugin.
|
||||
|
||||
4. **Verify DMs.** Send the agent a direct message; confirm the reply lands.
|
||||
|
||||
5. **Verify groups separately.** DMs and groups take different code paths — DM success does not prove groups are routing. Send the agent a message in a paired group chat and confirm the reply lands. If the group goes silent (no agent reply, no error), check the gateway log for `imessage: dropping group message from chat_id=<id>` or the startup `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty` line — both fire at the default log level. If either appears, your `groups` block is missing or empty — see "Group registry footgun" above.
|
||||
|
||||
6. **Verify the action surface** — from a paired DM, ask the agent to react, edit, unsend, reply, send a photo, and (in a group) rename the group / add or remove a participant. Each action should land natively in Messages.app. If any throws "iMessage `<action>` requires the imsg private API bridge", run `imsg launch` again and refresh `channels status --probe`.
|
||||
|
||||
7. **Stop the BlueBubbles server** once you have run on iMessage for at least a few hours of normal traffic. Remove the BlueBubbles block from config and restart the gateway.
|
||||
|
||||
## Action parity at a glance
|
||||
|
||||
| Action | BlueBubbles | bundled iMessage |
|
||||
| ---------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------ |
|
||||
| Send text / SMS fallback | ✅ | ✅ |
|
||||
| Send media (photo, video, file, voice) | ✅ | ✅ |
|
||||
| Threaded reply (`reply_to_guid`) | ✅ | ✅ (closes [#51892](https://github.com/openclaw/openclaw/issues/51892)) |
|
||||
| Tapback (`react`) | ✅ | ✅ |
|
||||
| Edit / unsend (macOS 13+ recipients) | ✅ | ✅ |
|
||||
| Send with screen effect | ✅ | ✅ (closes part of [#9394](https://github.com/openclaw/openclaw/issues/9394)) |
|
||||
| Rich text bold / italic / underline / strikethrough | ✅ | ✅ (typed-run formatting via attributedBody) |
|
||||
| Rename group / set group icon | ✅ | ✅ |
|
||||
| Add / remove participant, leave group | ✅ | ✅ |
|
||||
| Read receipts and typing indicator | ✅ | ✅ (gated on private API probe) |
|
||||
| Same-sender DM coalescing | ✅ | ✅ (DM-only; opt-in via `channels.imessage.coalesceSameSenderDms`) |
|
||||
| Catchup of inbound messages received while gateway is down | ✅ (webhook replay + history fetch) | _(not yet — tracked at [#78649](https://github.com/openclaw/openclaw/issues/78649))_ |
|
||||
|
||||
The catchup gap is the most operationally significant one for production deployments: planned restarts, mac sleep, or an unexpected gateway crash that takes more than a few seconds will silently drop any inbound iMessage traffic that arrives during the gap when running on bundled iMessage. BlueBubbles' webhook + history-fetch flow recovers those messages on reconnect. If your deployment is sensitive to that, stay on BlueBubbles until [#78649](https://github.com/openclaw/openclaw/issues/78649) lands.
|
||||
|
||||
## Pairing, sessions, and ACP bindings
|
||||
|
||||
- **Pairing approvals** carry over by handle. You do not need to re-approve known senders — `channels.imessage.allowFrom` recognizes the same `+15555550123` / `user@example.com` strings BlueBubbles used.
|
||||
- **Sessions** stay scoped per agent + chat. DMs collapse into the agent main session under default `session.dmScope=main`; group sessions stay isolated per `chat_id`. The session keys differ (`agent:<id>:imessage:group:<chat_id>` vs the BlueBubbles equivalent) — old conversation history under BlueBubbles session keys does not carry into iMessage sessions.
|
||||
- **ACP bindings** referencing `match.channel: "bluebubbles"` need to be updated to `"imessage"`. The `match.peer.id` shapes (`chat_id:`, `chat_guid:`, `chat_identifier:`, bare handle) are identical.
|
||||
|
||||
## Running both at once
|
||||
|
||||
You can keep both `bluebubbles` and `imessage` enabled during cutover testing. BlueBubbles' manifest still declares `preferOver: ["imessage"]`, so the auto-enable resolver continues to prefer BlueBubbles when both channels are configured — the bundled iMessage plugin will not pick up traffic until BlueBubbles is disabled (`channels.bluebubbles.enabled: false`) or removed from config.
|
||||
|
||||
If you want both channels to run simultaneously instead of in cutover mode, that is not currently supported through plugin auto-enable; use one channel at a time.
|
||||
|
||||
## Rollback
|
||||
|
||||
Because you kept the BlueBubbles config block:
|
||||
|
||||
1. Set `channels.bluebubbles.enabled: true` and `channels.imessage.enabled: false`.
|
||||
2. Restart the gateway.
|
||||
3. Inbound traffic returns to BlueBubbles. Reply caches and ACP bindings on the iMessage side stay on disk under `~/.openclaw/state/imessage/` and resume cleanly if you re-enable later.
|
||||
|
||||
The reply cache lives at `~/.openclaw/state/imessage/reply-cache.jsonl` (mode `0600`, parent dir `0700`). It is safe to delete if you want a clean slate.
|
||||
|
||||
## Related
|
||||
|
||||
- [iMessage](/channels/imessage) — full iMessage channel reference, including `imsg launch` setup and capability detection.
|
||||
- [BlueBubbles](/channels/bluebubbles) — full BlueBubbles channel reference for the legacy path.
|
||||
- [Pairing](/channels/pairing) — DM authentication and pairing flow.
|
||||
- [Channel Routing](/channels/channel-routing) — how the gateway picks a channel for outbound replies.
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Native iMessage support via imsg (JSON-RPC over stdio). Preferred for new OpenClaw iMessage setups when host requirements fit."
|
||||
summary: "Native iMessage support via imsg (JSON-RPC over stdio), with private API actions for replies, tapbacks, effects, attachments, and group management. Preferred for new OpenClaw iMessage setups when host requirements fit."
|
||||
read_when:
|
||||
- Setting up iMessage support
|
||||
- Debugging iMessage send/receive
|
||||
@@ -7,18 +7,27 @@ title: "iMessage"
|
||||
---
|
||||
|
||||
<Note>
|
||||
For new OpenClaw iMessage deployments, start here when you can run `imsg` on a signed-in macOS Messages host. BlueBubbles remains available as a legacy fallback for existing setups that depend on its HTTP server, webhooks, or richer private-API actions.
|
||||
For OpenClaw iMessage deployments, use `imsg` on a signed-in macOS Messages host. If your Gateway runs on Linux or Windows, point `channels.imessage.cliPath` at an SSH wrapper that runs `imsg` on the Mac.
|
||||
|
||||
**Known gap: no gateway-downtime catchup.** Messages that arrive while the gateway is down (crash, restart, Mac sleep, machine off) are not delivered to the agent once the gateway comes back up — `imsg watch` resumes from the current state and ignores anything that landed in `chat.db` during the gap. Tracked at [openclaw#78649](https://github.com/openclaw/openclaw/issues/78649).
|
||||
</Note>
|
||||
|
||||
Status: native external CLI integration. Gateway spawns `imsg rpc` and communicates over JSON-RPC on stdio (no separate daemon/port).
|
||||
<Warning>
|
||||
BlueBubbles is deprecated and no longer ships as a bundled OpenClaw channel. Migrate `channels.bluebubbles` configs to `channels.imessage`; OpenClaw now supports iMessage through `imsg` only. If you still need a BlueBubbles-backed bridge, publish or install it as a third-party plugin outside core.
|
||||
</Warning>
|
||||
|
||||
Status: native external CLI integration. Gateway spawns `imsg rpc` and communicates over JSON-RPC on stdio (no separate daemon/port). Advanced actions require `imsg launch` and a successful private API probe.
|
||||
|
||||
<CardGroup cols={3}>
|
||||
<Card title="BlueBubbles (legacy fallback)" icon="message-circle" href="/channels/bluebubbles">
|
||||
Keep using it for existing BlueBubbles-backed routing; avoid it for new setups when imsg fits.
|
||||
<Card title="Private API actions" icon="wand-sparkles" href="#private-api-actions">
|
||||
Replies, tapbacks, effects, attachments, and group management.
|
||||
</Card>
|
||||
<Card title="Pairing" icon="link" href="/channels/pairing">
|
||||
iMessage DMs default to pairing mode.
|
||||
</Card>
|
||||
<Card title="Remote Mac" icon="terminal" href="#remote-mac-over-ssh">
|
||||
Use an SSH wrapper when the Gateway is not running on the Messages Mac.
|
||||
</Card>
|
||||
<Card title="Configuration reference" icon="settings" href="/gateway/config-channels#imessage">
|
||||
Full iMessage field reference.
|
||||
</Card>
|
||||
@@ -34,6 +43,8 @@ Status: native external CLI integration. Gateway spawns `imsg rpc` and communica
|
||||
```bash
|
||||
brew install steipete/tap/imsg
|
||||
imsg rpc --help
|
||||
imsg launch
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
</Step>
|
||||
@@ -115,6 +126,7 @@ exec ssh -T gateway-host imsg "$@"
|
||||
- Messages must be signed in on the Mac running `imsg`.
|
||||
- Full Disk Access is required for the process context running OpenClaw/`imsg` (Messages DB access).
|
||||
- Automation permission is required to send messages through Messages.app.
|
||||
- For advanced actions (react / edit / unsend / threaded reply / effects / group ops), System Integrity Protection must be disabled — see [Enabling the imsg private API](#enabling-the-imsg-private-api) below. Basic text and media send/receive work without it.
|
||||
|
||||
<Tip>
|
||||
Permissions are granted per process context. If gateway runs headless (LaunchAgent/SSH), run a one-time interactive command in that same context to trigger prompts:
|
||||
@@ -127,6 +139,71 @@ imsg send <handle> "test"
|
||||
|
||||
</Tip>
|
||||
|
||||
## Enabling the imsg private API
|
||||
|
||||
`imsg` ships in two operational modes:
|
||||
|
||||
- **Basic mode** (default, no SIP changes needed): outbound text and media via `send`, inbound watch/history, chat list. This is what you get out of the box from a fresh `brew install steipete/tap/imsg` plus the standard macOS permissions above.
|
||||
- **Private API mode**: `imsg` injects a helper dylib into `Messages.app` to call internal `IMCore` functions. This is what unlocks `react`, `edit`, `unsend`, `reply` (threaded), `sendWithEffect`, `renameGroup`, `setGroupIcon`, `addParticipant`, `removeParticipant`, `leaveGroup`, plus typing indicators and read receipts.
|
||||
|
||||
To reach the advanced action surface that this channel page documents, you need Private API mode. The `imsg` README is explicit about the requirement:
|
||||
|
||||
> Advanced features such as `read`, `typing`, `launch`, bridge-backed rich send, message mutation, and chat management are opt-in. They require SIP to be disabled and a helper dylib to be injected into `Messages.app`. `imsg launch` refuses to inject when SIP is enabled.
|
||||
|
||||
The helper-injection technique is a manual port of the BlueBubbles private-API surface (Apache-2.0 inspired) into `imsg`'s own dylib — no third-party binary, but the same SIP-disabled requirement that BlueBubbles' Private API mode has. There is no SIP-asymmetry between the two channels.
|
||||
|
||||
<Warning>
|
||||
**Disabling SIP is a real security tradeoff.** SIP is one of macOS's core protections against running modified system code; turning it off system-wide opens up additional attack surface and side effects. Notably, **disabling SIP on Apple Silicon Macs also disables the ability to install and run iOS apps on your Mac**.
|
||||
|
||||
Treat this as a deliberate operational choice, not a default. If your threat model can't tolerate SIP being off, both bundled iMessage and BlueBubbles will be limited to their basic modes — text and media send/receive only, no reactions / edit / unsend / effects / group ops on either channel.
|
||||
</Warning>
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install (or upgrade) `imsg`** on the Mac that runs Messages.app:
|
||||
|
||||
```bash
|
||||
brew install steipete/tap/imsg
|
||||
imsg --version
|
||||
imsg status --json
|
||||
```
|
||||
|
||||
The `imsg status --json` output reports `bridge_version`, `rpc_methods`, and per-method `selectors` so you can see what the current build supports before you start.
|
||||
|
||||
2. **Disable System Integrity Protection.** This is macOS-version-specific, identical to the BlueBubbles flow because the underlying Apple requirement is the same:
|
||||
- **macOS 10.13–10.15 (Sierra–Catalina):** disable Library Validation via Terminal, reboot to Recovery Mode, run `csrutil disable`, restart.
|
||||
- **macOS 11+ (Big Sur and later), Intel:** Recovery Mode (or Internet Recovery), `csrutil disable`, restart.
|
||||
- **macOS 11+, Apple Silicon:** power-button startup sequence to enter Recovery; on recent macOS versions hold the **Left Shift** key when you click Continue, then `csrutil disable`. Virtual-machine setups follow a separate flow — take a VM snapshot first.
|
||||
- **macOS 26 / Tahoe:** library-validation policies and `imagent` private-entitlement checks have tightened further; `imsg` may need an updated build to keep up. If `imsg launch` injection or specific `selectors` start returning false after a macOS major upgrade, check `imsg`'s release notes before assuming the SIP step succeeded.
|
||||
|
||||
The [BlueBubbles Private API installation guide](https://docs.bluebubbles.app/private-api/installation) is the canonical step-by-step for the SIP-disable flow itself; the macOS-side steps are not specific to BB, only the helper that gets injected differs.
|
||||
|
||||
3. **Inject the helper.** With SIP disabled and Messages.app signed in:
|
||||
|
||||
```bash
|
||||
imsg launch
|
||||
```
|
||||
|
||||
`imsg launch` refuses to inject when SIP is still enabled, so this also doubles as a confirmation that step 2 took.
|
||||
|
||||
4. **Verify the bridge from OpenClaw:**
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
The iMessage entry should report `works`, and `imsg status --json | jq '.selectors'` should show `retractMessagePart: true` plus whichever edit / typing / read selectors your macOS build exposes. The OpenClaw plugin per-method gating in `actions.ts` only advertises actions whose underlying selector is `true`, so the action surface you see in the agent's tool list reflects what the bridge can actually do on this host.
|
||||
|
||||
If `openclaw channels status --probe` reports the channel as `works` but specific actions throw "iMessage `<action>` requires the imsg private API bridge" at dispatch time, run `imsg launch` again — the helper can fall out (Messages.app restart, OS update, etc.) and the cached `available: true` status will keep advertising actions until the next probe refreshes.
|
||||
|
||||
### When you can't disable SIP
|
||||
|
||||
If SIP-disabled isn't acceptable for your threat model:
|
||||
|
||||
- Both `imsg` and BlueBubbles fall back to basic mode — text + media + receive only.
|
||||
- The OpenClaw plugin still advertises text/media send and inbound monitoring; it just hides `react`, `edit`, `unsend`, `reply`, `sendWithEffect`, and group ops from the action surface (per the per-method capability gate).
|
||||
- You can run a separate non-Apple-Silicon Mac (or a dedicated bot Mac) with SIP off for the iMessage workload, while keeping SIP enabled on your primary devices. See [Dedicated bot macOS user (separate iMessage identity)](#deployment-patterns) below.
|
||||
|
||||
## Access control and routing
|
||||
|
||||
<Tabs>
|
||||
@@ -156,6 +233,36 @@ imsg send <handle> "test"
|
||||
Runtime fallback: if `groupAllowFrom` is unset, iMessage group sender checks fall back to `allowFrom` when available.
|
||||
Runtime note: if `channels.imessage` is completely missing, runtime falls back to `groupPolicy="allowlist"` and logs a warning (even if `channels.defaults.groupPolicy` is set).
|
||||
|
||||
<Warning>
|
||||
Group routing has **two** allowlist gates running back-to-back, and both must pass:
|
||||
|
||||
1. **Sender / chat-target allowlist** (`channels.imessage.groupAllowFrom`) — handle, `chat_guid`, `chat_identifier`, or `chat_id`.
|
||||
2. **Group registry** (`channels.imessage.groups`) — with `groupPolicy: "allowlist"`, this gate requires either a `groups: { "*": { ... } }` wildcard entry (sets `allowAll = true`), or an explicit per-`chat_id` entry under `groups`.
|
||||
|
||||
If gate 2 has nothing in it, every group message is dropped. The plugin emits two `warn`-level signals at the default log level:
|
||||
|
||||
- one-time per account at startup: `imessage: groupPolicy="allowlist" but channels.imessage.groups is empty for account "<id>"`
|
||||
- one-time per `chat_id` at runtime: `imessage: dropping group message from chat_id=<id> ...`
|
||||
|
||||
DMs continue to work because they take a different code path.
|
||||
|
||||
Minimum config to keep groups flowing under `groupPolicy: "allowlist"`:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
groupPolicy: "allowlist",
|
||||
groupAllowFrom: ["+15555550123"],
|
||||
groups: { "*": { "requireMention": true } },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If those `warn` lines appear in the gateway log, gate 2 is dropping — add the `groups` block.
|
||||
</Warning>
|
||||
|
||||
Mention gating for groups:
|
||||
|
||||
- iMessage has no native mention metadata
|
||||
@@ -260,24 +367,24 @@ See [ACP Agents](/tools/acp-agents) for shared ACP binding behavior.
|
||||
|
||||
Example:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
enabled: true,
|
||||
cliPath: "~/.openclaw/scripts/imsg-ssh",
|
||||
remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
|
||||
includeAttachments: true,
|
||||
dbPath: "/Users/bot/Library/Messages/chat.db",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
enabled: true,
|
||||
cliPath: "~/.openclaw/scripts/imsg-ssh",
|
||||
remoteHost: "bot@mac-mini.tailnet-1234.ts.net",
|
||||
includeAttachments: true,
|
||||
dbPath: "/Users/bot/Library/Messages/chat.db",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
|
||||
```
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
|
||||
```
|
||||
|
||||
Use SSH keys so both SSH and SCP are non-interactive.
|
||||
Ensure the host key is trusted first (for example `ssh bot@mac-mini.tailnet-1234.ts.net`) so `known_hosts` is populated.
|
||||
@@ -328,10 +435,76 @@ exec ssh -T bot@mac-mini.tailnet-1234.ts.net imsg "$@"
|
||||
- `sms:+1555...`
|
||||
- `user@example.com`
|
||||
|
||||
```bash
|
||||
imsg chats --limit 20
|
||||
```bash
|
||||
imsg chats --limit 20
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Private API actions
|
||||
|
||||
When `imsg launch` is running and `openclaw channels status --probe` reports `privateApi.available: true`, the message tool can use iMessage-native actions in addition to normal text sends.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
actions: {
|
||||
reactions: true,
|
||||
edit: true,
|
||||
unsend: true,
|
||||
reply: true,
|
||||
sendWithEffect: true,
|
||||
sendAttachment: true,
|
||||
renameGroup: true,
|
||||
setGroupIcon: true,
|
||||
addParticipant: true,
|
||||
removeParticipant: true,
|
||||
leaveGroup: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Available actions">
|
||||
- **react**: Add/remove iMessage tapbacks (`messageId`, `emoji`, `remove`). Supported tapbacks map to love, like, dislike, laugh, emphasize, and question.
|
||||
- **reply**: Send a threaded reply to an existing message (`messageId`, `text` or `message`, plus `chatGuid`, `chatId`, `chatIdentifier`, or `to`).
|
||||
- **sendWithEffect**: Send text with an iMessage effect (`text` or `message`, `effect` or `effectId`).
|
||||
- **edit**: Edit a sent message on supported macOS/private API versions (`messageId`, `text` or `newText`).
|
||||
- **unsend**: Retract a sent message on supported macOS/private API versions (`messageId`).
|
||||
- **upload-file**: Send media/files (`buffer` as base64 or a hydrated `media`/`path`/`filePath`, `filename`, optional `asVoice`). Legacy alias: `sendAttachment`.
|
||||
- **renameGroup**, **setGroupIcon**, **addParticipant**, **removeParticipant**, **leaveGroup**: Manage group chats when the current target is a group conversation.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Message IDs">
|
||||
Inbound iMessage context includes both short `MessageSid` values and full message GUIDs when available. Short IDs are scoped to the recent in-memory reply cache and are checked against the current chat before use. If a short ID has expired or belongs to another chat, retry with the full `MessageSidFull`.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Capability detection">
|
||||
OpenClaw hides private API actions only when the cached probe status says the bridge is unavailable. If the status is unknown, actions remain visible and dispatch probes lazily so the first action can succeed after `imsg launch` without a separate manual status refresh.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Read receipts and typing">
|
||||
When the private API bridge is up, accepted inbound chats are marked read before dispatch and a typing bubble is shown to the sender while the agent generates. Disable read-marking with:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
sendReadReceipts: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Older `imsg` builds that pre-date the per-method capability list will gate off typing/read silently; OpenClaw logs a one-time warning per restart so the missing receipt is attributable.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -351,18 +524,114 @@ Disable:
|
||||
}
|
||||
```
|
||||
|
||||
<a id="coalescing-split-send-dms-command--url-in-one-composition"></a>
|
||||
|
||||
## Coalescing split-send DMs (command + URL in one composition)
|
||||
|
||||
When a user types a command and a URL together — e.g. `Dump https://example.com/article` — Apple's Messages app splits the send into **two separate `chat.db` rows**:
|
||||
|
||||
1. A text message (`"Dump"`).
|
||||
2. A URL-preview balloon (`"https://..."`) with OG-preview images as attachments.
|
||||
|
||||
The two rows arrive at OpenClaw ~0.8-2.0 s apart on most setups. Without coalescing, the agent receives the command alone on turn 1, replies (often "send me the URL"), and only sees the URL on turn 2 — at which point the command context is already lost. This is Apple's send pipeline, not anything OpenClaw or `imsg` introduces, so the same fix applies as it does on the BlueBubbles channel.
|
||||
|
||||
`channels.imessage.coalesceSameSenderDms` opts a DM into merging consecutive same-sender rows into a single agent turn. Group chats continue to dispatch per-message so multi-user turn structure is preserved.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="When to enable">
|
||||
Enable when:
|
||||
|
||||
- You ship skills that expect `command + payload` in one message (dump, paste, save, queue, etc.).
|
||||
- Your users paste URLs, images, or long content alongside commands.
|
||||
- You can accept the added DM turn latency (see below).
|
||||
|
||||
Leave disabled when:
|
||||
|
||||
- You need minimum command latency for single-word DM triggers.
|
||||
- All your flows are one-shot commands without payload follow-ups.
|
||||
|
||||
</Tab>
|
||||
<Tab title="Enabling">
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
imessage: {
|
||||
coalesceSameSenderDms: true, // opt in (default: false)
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
With the flag on and no explicit `messages.inbound.byChannel.imessage`, the debounce window widens to **2500 ms** (the legacy default is 0 ms — no debouncing). The wider window is required because Apple's split-send cadence of 0.8-2.0 s does not fit in a tighter default.
|
||||
|
||||
To tune the window yourself:
|
||||
|
||||
```json5
|
||||
{
|
||||
messages: {
|
||||
inbound: {
|
||||
byChannel: {
|
||||
// 2500 ms works for most setups; raise to 4000 ms if your Mac is
|
||||
// slow or under memory pressure (observed gap can stretch past 2 s
|
||||
// then).
|
||||
imessage: 2500,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="Trade-offs">
|
||||
- **Added latency for DM messages.** With the flag on, every DM (including standalone control commands and single-text follow-ups) waits up to the debounce window before dispatching, in case a payload row is coming. Group-chat messages keep instant dispatch.
|
||||
- **Merged output is bounded.** Merged text caps at 4000 chars with an explicit `…[truncated]` marker; attachments cap at 20; source entries cap at 10 (first-plus-latest retained beyond that). Every source GUID is tracked in `coalescedMessageGuids` for downstream telemetry.
|
||||
- **DM-only.** Group chats fall through to per-message dispatch so the bot stays responsive when multiple people are typing.
|
||||
- **Opt-in, per-channel.** Other channels (Telegram, WhatsApp, Slack, …) are unaffected. The BlueBubbles channel has the same opt-in under `channels.bluebubbles.coalesceSameSenderDms`.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Scenarios and what the agent sees
|
||||
|
||||
| User composes | `chat.db` produces | Flag off (default) | Flag on + 2500 ms window |
|
||||
| ------------------------------------------------------------------ | --------------------- | --------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `Dump https://example.com` (one send) | 2 rows ~1 s apart | Two agent turns: "Dump" alone, then URL | One turn: merged text `Dump https://example.com` |
|
||||
| `Save this 📎image.jpg caption` (attachment + text) | 2 rows | Two turns (attachment dropped on merge) | One turn: text + image preserved |
|
||||
| `/status` (standalone command) | 1 row | Instant dispatch | **Wait up to window, then dispatch** |
|
||||
| URL pasted alone | 1 row | Instant dispatch | Instant dispatch (only one entry in bucket) |
|
||||
| Text + URL sent as two deliberate separate messages, minutes apart | 2 rows outside window | Two turns | Two turns (window expires between them) |
|
||||
| Rapid flood (>10 small DMs inside window) | N rows | N turns | One turn, bounded output (first + latest, text/attachment caps applied) |
|
||||
| Two people typing in a group chat | N rows from M senders | M+ turns (one per sender bucket) | M+ turns — group chats are not coalesced |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="imsg not found or RPC unsupported">
|
||||
Validate the binary and RPC support:
|
||||
|
||||
```bash
|
||||
imsg rpc --help
|
||||
imsg status --json
|
||||
openclaw channels status --probe
|
||||
```
|
||||
|
||||
If probe reports RPC unsupported, update `imsg`. If private API actions are unavailable, run `imsg launch` in the logged-in macOS user session and probe again. If the Gateway is not running on macOS, use the Remote Mac over SSH setup above instead of the default local `imsg` path.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Gateway is not running on macOS">
|
||||
The default `cliPath: "imsg"` must run on the Mac signed into Messages. On Linux or Windows, set `channels.imessage.cliPath` to a wrapper script that SSHes to that Mac and runs `imsg "$@"`.
|
||||
|
||||
```bash
|
||||
imsg rpc --help
|
||||
openclaw channels status --probe
|
||||
#!/usr/bin/env bash
|
||||
exec ssh -T messages-mac imsg "$@"
|
||||
```
|
||||
|
||||
If probe reports RPC unsupported, update `imsg`.
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
openclaw channels status --probe --channel imessage
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -399,10 +668,10 @@ openclaw channels status --probe
|
||||
<Accordion title="macOS permission prompts were missed">
|
||||
Re-run in an interactive GUI terminal in the same user/session context and approve prompts:
|
||||
|
||||
```bash
|
||||
imsg chats --limit 1
|
||||
imsg send <handle> "test"
|
||||
```
|
||||
```bash
|
||||
imsg chats --limit 1
|
||||
imsg send <handle> "test"
|
||||
```
|
||||
|
||||
Confirm Full Disk Access + Automation are granted for the process context that runs OpenClaw/`imsg`.
|
||||
|
||||
@@ -414,11 +683,11 @@ imsg send <handle> "test"
|
||||
- [Configuration reference - iMessage](/gateway/config-channels#imessage)
|
||||
- [Gateway configuration](/gateway/configuration)
|
||||
- [Pairing](/channels/pairing)
|
||||
- [BlueBubbles](/channels/bluebubbles)
|
||||
|
||||
## Related
|
||||
|
||||
- [Channels Overview](/channels) — all supported channels
|
||||
- [Coming from BlueBubbles](/channels/imessage-from-bluebubbles) — config translation table and step-by-step cutover
|
||||
- [Pairing](/channels/pairing) — DM authentication and pairing flow
|
||||
- [Groups](/channels/groups) — group chat behavior and mention gating
|
||||
- [Channel Routing](/channels/channel-routing) — session routing for messages
|
||||
|
||||
@@ -21,11 +21,10 @@ Text is supported everywhere; media and reactions vary by channel.
|
||||
|
||||
## Supported channels
|
||||
|
||||
- [BlueBubbles](/channels/bluebubbles) - Legacy iMessage bridge via the BlueBubbles macOS server REST API; deprecated for new OpenClaw setups but still supported for existing configs and richer private-API actions.
|
||||
- [Discord](/channels/discord) - Discord Bot API + Gateway; supports servers, channels, and DMs.
|
||||
- [Feishu](/channels/feishu) - Feishu/Lark bot via WebSocket (bundled plugin).
|
||||
- [Google Chat](/channels/googlechat) - Google Chat API app via HTTP webhook (downloadable plugin).
|
||||
- [iMessage](/channels/imessage) - Native macOS integration via the imsg CLI; preferred for new OpenClaw iMessage setups when host permissions and Messages access fit.
|
||||
- [iMessage](/channels/imessage) - Native macOS integration via the `imsg` bridge on a signed-in Mac (or SSH wrapper when the Gateway runs elsewhere), including private API actions for replies, tapbacks, effects, attachments, and group management. Preferred for new OpenClaw iMessage setups when host permissions and Messages access fit.
|
||||
- [IRC](/channels/irc) - Classic IRC servers; channels + DMs with pairing/allowlist controls.
|
||||
- [LINE](/channels/line) - LINE Messaging API bot (downloadable plugin).
|
||||
- [Matrix](/channels/matrix) - Matrix protocol (downloadable plugin).
|
||||
|
||||
@@ -752,6 +752,27 @@ Teams recently introduced two channel UI styles over the same underlying data mo
|
||||
}
|
||||
```
|
||||
|
||||
### Resolution precedence
|
||||
|
||||
When the bot sends a reply into a channel, `replyStyle` is resolved from the most specific override down to the default. The first non-`undefined` value wins:
|
||||
|
||||
1. **Per-channel** — `channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle`
|
||||
2. **Per-team** — `channels.msteams.teams.<teamId>.replyStyle`
|
||||
3. **Global** — `channels.msteams.replyStyle`
|
||||
4. **Implicit default** — derived from `requireMention`:
|
||||
- `requireMention: true` → `thread`
|
||||
- `requireMention: false` → `top-level`
|
||||
|
||||
If you set `requireMention: false` globally without an explicit `replyStyle`, mentions in Posts-style channels will surface as top-level posts even when the inbound was a thread reply. Pin `replyStyle: "thread"` at the global, team, or channel level to avoid surprises.
|
||||
|
||||
### Thread context preservation
|
||||
|
||||
When `replyStyle: "thread"` is in effect and the bot was @mentioned from inside a channel thread, OpenClaw re-attaches the original thread root to the outbound conversation reference (`19:…@thread.tacv2;messageid=<root>`) so the reply lands inside the same thread. This holds for both live (in-turn) sends and proactive sends made after the Bot Framework turn context has expired (e.g., long-running agents, queued tool-call replies via `mcp__openclaw__message`).
|
||||
|
||||
The thread root is taken from the stored `threadId` on the conversation reference. Older stored references that predate `threadId` fall back to `activityId` (whatever inbound activity last seeded the conversation), so existing deployments keep working without a re-seed.
|
||||
|
||||
When `replyStyle: "top-level"` is in effect, channel-thread inbounds are intentionally answered as new top-level posts — no thread suffix is attached. This is the correct behavior for Threads-style channels; if you see top-level posts where you expected threaded replies, your `replyStyle` is set incorrectly for that channel.
|
||||
|
||||
## Attachments and images
|
||||
|
||||
**Current limitations:**
|
||||
|
||||
@@ -45,7 +45,7 @@ That gives first-time setups an explicit owner for privileged commands and exec
|
||||
approval prompts. After an owner exists, later pairing approvals only grant DM
|
||||
access; they do not add more owners.
|
||||
|
||||
Supported channels: `bluebubbles`, `discord`, `feishu`, `googlechat`, `imessage`, `irc`, `line`, `matrix`, `mattermost`, `msteams`, `nextcloud-talk`, `nostr`, `openclaw-weixin`, `signal`, `slack`, `synology-chat`, `telegram`, `twitch`, `whatsapp`, `zalo`, `zalouser`.
|
||||
Supported channels: `discord`, `feishu`, `googlechat`, `imessage`, `irc`, `line`, `matrix`, `mattermost`, `msteams`, `nextcloud-talk`, `nostr`, `openclaw-weixin`, `signal`, `slack`, `synology-chat`, `telegram`, `twitch`, `whatsapp`, `zalo`, `zalouser`.
|
||||
|
||||
### Reusable sender groups
|
||||
|
||||
@@ -209,6 +209,5 @@ Stored under `~/.openclaw/devices/`:
|
||||
- WhatsApp: [WhatsApp](/channels/whatsapp)
|
||||
- Signal: [Signal](/channels/signal)
|
||||
- iMessage: [iMessage](/channels/imessage)
|
||||
- BlueBubbles (legacy iMessage bridge): [BlueBubbles](/channels/bluebubbles)
|
||||
- Discord: [Discord](/channels/discord)
|
||||
- Slack: [Slack](/channels/slack)
|
||||
|
||||
@@ -82,20 +82,19 @@ Full troubleshooting: [Discord troubleshooting](/channels/discord#troubleshootin
|
||||
|
||||
Full troubleshooting: [Slack troubleshooting](/channels/slack#troubleshooting)
|
||||
|
||||
## iMessage and BlueBubbles
|
||||
## iMessage
|
||||
|
||||
### iMessage and BlueBubbles failure signatures
|
||||
### iMessage failure signatures
|
||||
|
||||
| Symptom | Fastest check | Fix |
|
||||
| -------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------- |
|
||||
| No inbound events | Verify webhook/server reachability and app permissions | Fix webhook URL or BlueBubbles server state. |
|
||||
| Can send but no receive on macOS | Check macOS privacy permissions for Messages automation | Re-grant TCC permissions and restart channel process. |
|
||||
| DM sender blocked | `openclaw pairing list imessage` or `openclaw pairing list bluebubbles` | Approve pairing or update allowlist. |
|
||||
| Symptom | Fastest check | Fix |
|
||||
| ------------------------------------ | ------------------------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `imsg` missing or fails on non-macOS | `openclaw channels status --probe --channel imessage` | Run OpenClaw on the Messages Mac or use an SSH wrapper for `cliPath`. |
|
||||
| Can send but no receive on macOS | Check macOS privacy permissions for Messages automation | Re-grant TCC permissions and restart channel process. |
|
||||
| DM sender blocked | `openclaw pairing list imessage` | Approve pairing or update allowlist. |
|
||||
|
||||
Full troubleshooting:
|
||||
|
||||
- [iMessage troubleshooting](/channels/imessage#troubleshooting)
|
||||
- [BlueBubbles troubleshooting](/channels/bluebubbles#troubleshooting)
|
||||
|
||||
## Signal
|
||||
|
||||
|
||||
96
docs/clawhub/publishing.md
Normal file
96
docs/clawhub/publishing.md
Normal file
@@ -0,0 +1,96 @@
|
||||
---
|
||||
summary: "How ClawHub publishing works for skills, plugins, owners, scopes, releases, and review."
|
||||
read_when:
|
||||
- Publishing a skill or plugin
|
||||
- Debugging owner or package scope errors
|
||||
- Adding publish UI, CLI, or backend behavior
|
||||
---
|
||||
|
||||
# Publishing on ClawHub
|
||||
|
||||
ClawHub publishing is owner-scoped: every publish targets a publisher, and the
|
||||
server decides whether the signed-in user is allowed to publish there.
|
||||
|
||||
## Owners
|
||||
|
||||
An owner is a ClawHub publisher handle, such as `@alice` or `@openclaw`.
|
||||
Personal owners are created for users. Org owners can have multiple members.
|
||||
|
||||
When you publish, you either use your personal owner or choose an org owner
|
||||
where you have publisher access.
|
||||
|
||||
## Skills
|
||||
|
||||
Skills are published from a skill folder. The public page is:
|
||||
|
||||
```text
|
||||
https://clawhub.ai/<owner>/<slug>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
https://clawhub.ai/alice/review-helper
|
||||
```
|
||||
|
||||
The publish request includes the selected owner, slug, version, changelog, and
|
||||
files. The server verifies that the actor can publish as that owner before it
|
||||
creates the release.
|
||||
|
||||
## Plugins
|
||||
|
||||
Plugins use npm-style package names. Scoped package names include the owner in
|
||||
the first part of the name:
|
||||
|
||||
```text
|
||||
@owner/package-name
|
||||
```
|
||||
|
||||
The scope must match the selected publish owner. If your package is named
|
||||
`@openclaw/dronzer`, it can only be published as `@openclaw`. If you publish as
|
||||
`@vintageayu`, rename the package to `@vintageayu/dronzer`.
|
||||
|
||||
This prevents a package from claiming an org namespace that the publisher does
|
||||
not control.
|
||||
|
||||
## Release Flow
|
||||
|
||||
1. The UI, CLI, or GitHub workflow gathers package metadata and files.
|
||||
2. The publish request is sent to ClawHub with the selected owner.
|
||||
3. The server validates owner permissions, package scope, package name, version,
|
||||
file limits, and source metadata.
|
||||
4. ClawHub stores the release and starts automated security checks.
|
||||
5. New releases are hidden from normal install/download surfaces until review
|
||||
and verification finish.
|
||||
|
||||
If validation fails, the release is not created.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Package scope must match selected owner
|
||||
|
||||
If the package scope and selected owner do not match, ClawHub rejects the
|
||||
publish:
|
||||
|
||||
```text
|
||||
Package scope "@openclaw" must match selected owner "@vintageayu".
|
||||
Publish as "@openclaw" or rename this package to "@vintageayu/dronzer".
|
||||
```
|
||||
|
||||
To fix it, either choose the owner named by the package scope, or rename the
|
||||
package so the scope matches the owner you can publish as.
|
||||
|
||||
If the package name already has the right scope but the package is owned by the
|
||||
wrong publisher, transfer ownership instead:
|
||||
|
||||
```sh
|
||||
clawhub package transfer @opik/opik-openclaw --to opik
|
||||
```
|
||||
|
||||
Use package transfer only when you have admin access to both the current package
|
||||
owner and the destination publisher. It does not let you publish into a scope you
|
||||
cannot manage.
|
||||
|
||||
This protects org namespaces. A package named `@openclaw/dronzer` claims the
|
||||
`@openclaw` namespace, so only publishers with access to the `@openclaw` owner
|
||||
can publish it.
|
||||
@@ -42,7 +42,8 @@ Quick rule:
|
||||
| ACP area | Status | Notes |
|
||||
| --------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `initialize`, `newSession`, `prompt`, `cancel` | Implemented | Core bridge flow over stdio to Gateway chat/send + abort. |
|
||||
| `listSessions`, slash commands | Implemented | Session list works against Gateway session state; commands are advertised via `available_commands_update`. |
|
||||
| `listSessions`, slash commands | Implemented | Session list works against Gateway session state with bounded cursor pagination and `cwd` filtering where Gateway session rows carry workspace metadata; commands are advertised via `available_commands_update`. |
|
||||
| `resumeSession`, `closeSession` | Implemented | Resume rebinds an ACP session to an existing Gateway session without replaying history. Close cancels active bridge work, resolves pending prompts as cancelled, and releases bridge session state. |
|
||||
| `loadSession` | Partial | Rebinds the ACP session to a Gateway session key and replays stored user/assistant text history. Tool/system history is not reconstructed yet. |
|
||||
| Prompt content (`text`, embedded `resource`, images) | Partial | Text/resources are flattened into chat input; images become Gateway attachments. |
|
||||
| Session modes | Partial | `session/set_mode` is supported and the bridge exposes initial Gateway-backed session controls for thought level, tool verbosity, reasoning, usage detail, and elevated actions. Broader ACP-native mode/config surfaces are still out of scope. |
|
||||
@@ -120,6 +121,50 @@ Permission model (client debug mode):
|
||||
- Server-provided `toolCall.kind` is treated as untrusted metadata (not an authorization source).
|
||||
- This ACP bridge policy is separate from ACPX harness permissions. If you run OpenClaw through the `acpx` backend, `plugins.entries.acpx.config.permissionMode=approve-all` is the break-glass "yolo" switch for that harness session.
|
||||
|
||||
## Protocol smoke testing
|
||||
|
||||
For protocol-level debugging, start a Gateway with isolated state and drive
|
||||
`openclaw acp` over stdio with an ACP JSON-RPC client. Cover `initialize`,
|
||||
`session/new`, `session/list` with an absolute `cwd`, `session/resume`,
|
||||
`session/close`, duplicate close, and missing resume.
|
||||
|
||||
The proof should include the advertised lifecycle capabilities, a Gateway-backed
|
||||
session row, update notifications, and the Gateway `sessions.list` log:
|
||||
|
||||
```json
|
||||
{
|
||||
"initialize": {
|
||||
"protocolVersion": 1,
|
||||
"agentCapabilities": {
|
||||
"sessionCapabilities": {
|
||||
"list": {},
|
||||
"resume": {},
|
||||
"close": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"listSessions": {
|
||||
"sessions": [
|
||||
{
|
||||
"sessionId": "agent:main:acp-smoke",
|
||||
"cwd": "/path/to/workspace",
|
||||
"_meta": {
|
||||
"sessionKey": "agent:main:acp-smoke",
|
||||
"kind": "direct"
|
||||
}
|
||||
}
|
||||
],
|
||||
"nextCursor": null
|
||||
},
|
||||
"notifications": ["session_info_update", "available_commands_update", "usage_update"],
|
||||
"gatewayLogTail": ["[gateway] ready", "[ws] ⇄ res ✓ sessions.list 305ms"]
|
||||
}
|
||||
```
|
||||
|
||||
Avoid using `openclaw gateway call sessions.list` as the only ACP proof. That
|
||||
CLI path may request a fresh-token operator scope upgrade; ACP bridge
|
||||
correctness is proven by ACP stdio frames plus the Gateway `sessions.list` log.
|
||||
|
||||
## How to use this
|
||||
|
||||
Use ACP when an IDE (or other client) speaks Agent Client Protocol and you want
|
||||
|
||||
@@ -19,6 +19,7 @@ Related docs:
|
||||
|
||||
```bash
|
||||
openclaw channels list
|
||||
openclaw channels list --all
|
||||
openclaw channels status
|
||||
openclaw channels capabilities
|
||||
openclaw channels capabilities --channel discord --target channel:123
|
||||
@@ -27,6 +28,8 @@ openclaw channels resolve --channel slack "#general" "@jane"
|
||||
openclaw channels logs --channel all
|
||||
```
|
||||
|
||||
`channels list` shows chat channels only: configured accounts by default, with `installed`, `configured`, and `enabled` status tags per account. Pass `--all` to also surface bundled channels that have no configured account yet and installable catalog channels that are not yet on disk. Auth providers (OAuth + API keys) and model-provider usage/quota snapshots are no longer printed here; use `openclaw models auth list` for provider auth profiles and `openclaw status` or `openclaw models list` for usage.
|
||||
|
||||
## Status / capabilities / resolve / logs
|
||||
|
||||
- `channels status`: `--probe`, `--timeout <ms>`, `--json`
|
||||
@@ -109,7 +112,7 @@ openclaw channels logout --channel whatsapp
|
||||
|
||||
- Run `openclaw status --deep` for a broad probe.
|
||||
- Use `openclaw doctor` for guided fixes.
|
||||
- `openclaw channels list` prints `Claude: HTTP 403 ... user:profile` → usage snapshot needs the `user:profile` scope. Use `--no-usage`, or provide a claude.ai session key (`CLAUDE_WEB_SESSION_KEY` / `CLAUDE_WEB_COOKIE`), or re-auth via Claude CLI.
|
||||
- `openclaw channels list` no longer prints model provider usage/quota snapshots. For those, use `openclaw status` (overview) or `openclaw models list` (per-provider).
|
||||
- `openclaw channels status` falls back to config-only summaries when the gateway is unreachable. If a supported channel credential is configured via SecretRef but unavailable in the current command path, it reports that account as configured with degraded notes instead of showing it as not configured.
|
||||
|
||||
## Capabilities probe
|
||||
|
||||
@@ -157,8 +157,6 @@ Retention and pruning are controlled in config:
|
||||
|
||||
<Note>
|
||||
If you have cron jobs from before the current delivery and store format, run `openclaw doctor --fix`. Doctor normalizes legacy cron fields (`jobId`, `schedule.cron`, top-level delivery fields including legacy `threadId`, payload `provider` delivery aliases) and migrates simple `notify: true` webhook fallback jobs to explicit webhook delivery when `cron.webhook` is configured.
|
||||
|
||||
Doctor also removes persisted cron `payload.model` sentinels such as `"default"`, `"null"`, blank strings, and JSON `null`. Cron runtime still treats any non-empty `payload.model` string as an explicit model override and validates it against `agents.defaults.models`; omit the model key when a job should use the agent/default model selection.
|
||||
</Note>
|
||||
|
||||
## Common edits
|
||||
@@ -222,6 +220,8 @@ openclaw cron runs --id <job-id> --limit 50
|
||||
|
||||
`openclaw cron list` shows all matching jobs by default. Pass `--agent <id>` to show only jobs whose effective normalized agent id matches; jobs without a stored agent id count as the configured default agent.
|
||||
|
||||
`cron list --json` and `cron show <job-id> --json` include a top-level `status` field on each job, computed from `enabled`, `state.runningAtMs`, and `state.lastRunStatus`. Values: `disabled`, `running`, `ok`, `error`, `skipped`, or `idle`. This mirrors the human-readable status column so external tooling can read job state without re-deriving it.
|
||||
|
||||
`cron runs` entries include delivery diagnostics with the intended cron target, the resolved target, message-tool sends, fallback use, and delivered state.
|
||||
|
||||
Agent and session retargeting:
|
||||
|
||||
@@ -21,9 +21,11 @@ openclaw migrate list
|
||||
openclaw migrate claude --dry-run
|
||||
openclaw migrate codex --dry-run
|
||||
openclaw migrate codex --skill gog-vault77-google-workspace
|
||||
openclaw migrate codex --plugin google-calendar --dry-run
|
||||
openclaw migrate hermes --dry-run
|
||||
openclaw migrate hermes
|
||||
openclaw migrate apply codex --yes --skill gog-vault77-google-workspace
|
||||
openclaw migrate apply codex --yes --plugin google-calendar
|
||||
openclaw migrate apply codex --yes
|
||||
openclaw migrate apply claude --yes
|
||||
openclaw migrate apply hermes --yes
|
||||
@@ -54,6 +56,9 @@ openclaw onboard --import-from hermes --import-source ~/.hermes
|
||||
<ParamField path="--skill <name>" type="string">
|
||||
Select one skill copy item by skill name or item id. Repeat the flag to migrate multiple skills. When omitted, interactive Codex migrations show a checkbox selector and non-interactive migrations keep all planned skills.
|
||||
</ParamField>
|
||||
<ParamField path="--plugin <name>" type="string">
|
||||
Select one Codex plugin install item by plugin name or item id. Repeat the flag to migrate multiple Codex plugins. This only applies to source-installed `openai-curated` Codex plugins discovered by the Codex app-server inventory.
|
||||
</ParamField>
|
||||
<ParamField path="--no-backup" type="boolean">
|
||||
Skip the pre-apply backup. Requires `--force` when local OpenClaw state exists.
|
||||
</ParamField>
|
||||
@@ -129,20 +134,51 @@ openclaw migrate codex --dry-run --skill gog-vault77-google-workspace
|
||||
openclaw migrate apply codex --yes --skill gog-vault77-google-workspace
|
||||
```
|
||||
|
||||
Use `--plugin <name>` to limit native Codex plugin migration to one or more
|
||||
source-installed curated plugins:
|
||||
|
||||
```bash
|
||||
openclaw migrate codex --dry-run --plugin google-calendar
|
||||
openclaw migrate apply codex --yes --plugin google-calendar
|
||||
```
|
||||
|
||||
### What Codex imports
|
||||
|
||||
- Codex CLI skill directories under `$CODEX_HOME/skills`, excluding Codex's
|
||||
`.system` cache.
|
||||
- Personal AgentSkills under `$HOME/.agents/skills`, copied into the current
|
||||
OpenClaw agent workspace when you want per-agent ownership.
|
||||
- Source-installed `openai-curated` Codex plugins discovered through Codex
|
||||
app-server `plugin/list`. Apply calls app-server `plugin/install` for each
|
||||
selected plugin, even if the target app-server already reports that plugin as
|
||||
installed and enabled. Migrated Codex plugins are usable only in sessions that
|
||||
select the native Codex harness; they are not exposed to Pi, normal OpenAI
|
||||
provider runs, ACP conversation bindings, or other harnesses.
|
||||
|
||||
### Manual-review Codex state
|
||||
|
||||
Codex native plugins, `config.toml`, and native `hooks/hooks.json` are not
|
||||
activated automatically. Plugins may expose MCP servers, apps, hooks, or other
|
||||
executable behavior, so the provider reports them for review instead of loading
|
||||
them into OpenClaw. Config and hook files are copied into the migration report
|
||||
for manual review.
|
||||
Codex `config.toml`, native `hooks/hooks.json`, non-curated marketplaces, and
|
||||
cached plugin bundles that are not source-installed curated plugins are not
|
||||
activated automatically. They are copied or reported in the migration report for
|
||||
manual review.
|
||||
|
||||
For migrated source-installed curated plugins, apply writes:
|
||||
|
||||
- `plugins.entries.codex.enabled: true`
|
||||
- `plugins.entries.codex.config.codexPlugins.enabled: true`
|
||||
- `plugins.entries.codex.config.codexPlugins.allow_destructive_actions: false`
|
||||
- one explicit plugin entry with `marketplaceName: "openai-curated"` and
|
||||
`pluginName` for each selected plugin
|
||||
|
||||
Migration never writes `plugins["*"]` and never stores local marketplace cache
|
||||
paths. Auth-required installs are reported on the affected plugin item with
|
||||
`status: "skipped"`, `reason: "auth_required"`, and sanitized app identifiers.
|
||||
Their explicit config entries are written disabled until you reauthorize and
|
||||
enable them. Other install failures are item-scoped `error` results.
|
||||
|
||||
If Codex app-server plugin inventory is unavailable during planning, migration
|
||||
falls back to cached bundle advisory items instead of failing the whole
|
||||
migration.
|
||||
|
||||
## Hermes provider
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ Probe rows can come from auth profiles, env credentials, or `models.json`.
|
||||
For Codex OAuth troubleshooting, `openclaw models status`,
|
||||
`openclaw models auth list --provider openai-codex`, and
|
||||
`openclaw config get agents.defaults.model --json` are the quickest way to
|
||||
confirm whether an agent is using `openai-codex/*` through PI or `openai/*`
|
||||
through the native Codex runtime. See [OpenAI provider setup](/providers/openai#check-and-recover-codex-oauth-routing).
|
||||
confirm whether an agent has a usable `openai-codex` auth profile for
|
||||
`openai/*` through the native Codex runtime. See [OpenAI provider setup](/providers/openai#check-and-recover-codex-oauth-routing).
|
||||
|
||||
Notes:
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ Invoke flags:
|
||||
|
||||
For shell execution on a node, use the `exec` tool with `host=node` instead of `openclaw nodes run`.
|
||||
The `nodes` CLI is now capability-focused: direct RPC via `nodes invoke`, plus pairing, camera,
|
||||
screen, location, canvas, and notifications.
|
||||
screen, location, Canvas, and notifications. Canvas commands are implemented by the bundled experimental Canvas plugin; core keeps a compatibility hook so they remain under `openclaw nodes canvas`.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ is available, then fall back to `latest`.
|
||||
|
||||
This CLI flag applies to plugin install/update flows. Gateway-backed skill dependency installs use the matching `dangerouslyForceUnsafeInstall` request override, while `openclaw skills install` remains a separate ClawHub skill download/install flow.
|
||||
|
||||
If a plugin you published on ClawHub is blocked by a registry scan, use the publisher steps in [ClawHub](/tools/clawhub).
|
||||
If a plugin you published on ClawHub is blocked by a registry scan, use the publisher steps in [ClawHub](/clawhub/security).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Hook packs and npm specs">
|
||||
@@ -139,7 +139,7 @@ is available, then fall back to `latest`.
|
||||
|
||||
Use `npm:<package>` when you want to make npm resolution explicit. Bare package specs also install directly from npm during the launch cutover.
|
||||
|
||||
Bare specs and `@latest` stay on the stable track. Legacy OpenClaw correction versions such as `2026.5.3-1` are still treated as stable releases for this check so older packages keep updating safely. New monthly support-line work is planned to use normal SemVer patch numbers instead of hyphen correction suffixes. If npm resolves a default-line spec to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`.
|
||||
Bare specs and `@latest` stay on the stable track. OpenClaw date-stamped correction versions such as `2026.5.3-1` are stable releases for this check. If npm resolves either of those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a prerelease tag such as `@beta`/`@rc` or an exact prerelease version such as `@1.2.3-beta.4`.
|
||||
|
||||
If a bare install spec matches an official plugin id (for example `diffs`), OpenClaw installs the catalog entry directly. To install an npm package with the same name, use an explicit scoped spec (for example `@scope/diffs`).
|
||||
|
||||
@@ -337,8 +337,6 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked
|
||||
<Accordion title="Beta channel updates">
|
||||
`openclaw plugins update` reuses the tracked plugin spec unless you pass a new spec. `openclaw update` additionally knows the active OpenClaw update channel: on the beta channel, default-line npm and ClawHub plugin records try `@beta` first, then fall back to the recorded default/latest spec if no plugin beta release exists. Exact versions and explicit tags stay pinned to that selector.
|
||||
|
||||
OpenClaw does not yet expose LTS or monthly support plugin channels. Planned support-line work will need plugin package and ClawHub tags to follow the same support line as the core package.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Version checks and integrity drift">
|
||||
Before a live npm update, OpenClaw checks the installed package version against the npm registry metadata. If the installed version and recorded artifact identity already match the resolved target, the update is skipped without downloading, reinstalling, or rewriting `openclaw.json`.
|
||||
@@ -361,7 +359,7 @@ openclaw plugins inspect <id> --json
|
||||
|
||||
Inspect shows identity, load status, source, manifest capabilities, policy flags, diagnostics, install metadata, bundle capabilities, and any detected MCP or LSP server support without importing plugin runtime by default. Add `--runtime` to load the plugin module and include registered hooks, tools, commands, services, gateway methods, and HTTP routes. Runtime inspection reports missing plugin dependencies directly; installs and repairs stay in `openclaw plugins install`, `openclaw plugins update`, and `openclaw doctor --fix`.
|
||||
|
||||
Plugin-owned CLI commands are installed as root `openclaw` command groups. After `inspect --runtime` shows a command under `cliCommands`, run it as `openclaw <command> ...`; for example a plugin that registers `demo-git` can be verified with `openclaw demo-git ping`.
|
||||
Plugin-owned CLI commands are usually installed as root `openclaw` command groups, but plugins may also register nested commands under a core parent such as `openclaw nodes`. After `inspect --runtime` shows a command under `cliCommands`, run it at the listed path; for example a plugin that registers `demo-git` can be verified with `openclaw demo-git ping`.
|
||||
|
||||
Each plugin is classified by what it actually registers at runtime:
|
||||
|
||||
@@ -419,4 +417,4 @@ Marketplace list accepts a local marketplace path, a `marketplace.json` path, a
|
||||
|
||||
- [Building plugins](/plugins/building-plugins)
|
||||
- [CLI reference](/cli)
|
||||
- [Community plugins](/plugins/community)
|
||||
- [ClawHub](/clawhub)
|
||||
|
||||
@@ -33,7 +33,7 @@ It also emits `security.trust_model.multi_user_heuristic` when config suggests l
|
||||
For intentional shared-user setups, the audit guidance is to sandbox all sessions, keep filesystem access workspace-scoped, and keep personal/private identities or credentials off that runtime.
|
||||
It also warns when small models (`<=300B`) are used without sandboxing and with web/browser tools enabled.
|
||||
For webhook ingress, it warns when `hooks.token` reuses the Gateway token, when `hooks.token` is short, when `hooks.path="/"`, when `hooks.defaultSessionKey` is unset, when `hooks.allowedAgentIds` is unrestricted, when request `sessionKey` overrides are enabled, and when overrides are enabled without `hooks.allowedSessionKeyPrefixes`.
|
||||
It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries (exact node command-name matching only, not shell-text filtering), when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed plugin tools may be reachable under permissive tool policy.
|
||||
It also warns when sandbox Docker settings are configured while sandbox mode is off, when `gateway.nodes.denyCommands` uses ineffective pattern-like/unknown entries (exact node command-name matching only, not shell-text filtering), when `gateway.nodes.allowCommands` explicitly enables dangerous node commands, when global `tools.profile="minimal"` is overridden by agent tool profiles, when write/edit tools are disabled but `exec` is still available without a constraining sandbox filesystem boundary, when open groups expose runtime/filesystem tools without sandbox/workspace guards, and when installed plugin tools may be reachable under permissive tool policy.
|
||||
It also flags `gateway.allowRealIpFallback=true` (header-spoofing risk if proxies are misconfigured) and `discovery.mdns.mode="full"` (metadata leakage via mDNS TXT records).
|
||||
It also warns when sandbox browser uses Docker `bridge` network without `sandbox.browser.cdpSourceRange`.
|
||||
It also flags dangerous sandbox Docker network modes (including `host` and `container:*` namespace joins).
|
||||
|
||||
@@ -23,6 +23,11 @@ event loop. The CLI returns the newest 100 sessions by default; pass
|
||||
need the full store. JSON responses include `totalCount`, `limitApplied`, and
|
||||
`hasMore` when callers need to show that more rows exist.
|
||||
|
||||
RPC clients can pass `configuredAgentsOnly: true` to keep the broad combined
|
||||
discovery source but return only rows for agents currently present in config.
|
||||
Control UI uses that mode by default so deleted or disk-only agent stores do
|
||||
not reappear in the Sessions view.
|
||||
|
||||
```bash
|
||||
openclaw sessions
|
||||
openclaw sessions --agent work
|
||||
@@ -93,6 +98,7 @@ openclaw sessions cleanup --agent work --dry-run
|
||||
openclaw sessions cleanup --all-agents --dry-run
|
||||
openclaw sessions cleanup --enforce
|
||||
openclaw sessions cleanup --enforce --active-key "agent:main:telegram:direct:123"
|
||||
openclaw sessions cleanup --dry-run --fix-dm-scope
|
||||
openclaw sessions cleanup --json
|
||||
```
|
||||
|
||||
@@ -105,6 +111,7 @@ openclaw sessions cleanup --json
|
||||
- In text mode, dry-run prints a per-session action table (`Action`, `Key`, `Age`, `Model`, `Flags`) so you can see what would be kept vs removed.
|
||||
- `--enforce`: apply maintenance even when `session.maintenance.mode` is `warn`.
|
||||
- `--fix-missing`: remove entries whose transcript files are missing, even if they would not normally age/count out yet.
|
||||
- `--fix-dm-scope`: when `session.dmScope` is `main`, retire stale peer-keyed direct-DM rows left behind by earlier `per-peer`, `per-channel-peer`, or `per-account-channel-peer` routing. Use `--dry-run` first; applying the cleanup removes those rows from `sessions.json` and preserves their transcripts as deleted archives.
|
||||
- `--active-key <key>`: protect a specific active key from disk-budget eviction. Durable external conversation pointers, such as group sessions and thread-scoped chat sessions, are also kept by age/count/disk-budget maintenance.
|
||||
- `--agent <id>`: run cleanup for one configured agent store.
|
||||
- `--all-agents`: run cleanup for all configured agent stores.
|
||||
@@ -128,6 +135,8 @@ traffic. Use `--store <path>` for explicit offline repair of a store file.
|
||||
"storePath": "/home/user/.openclaw/agents/main/sessions/sessions.json",
|
||||
"beforeCount": 120,
|
||||
"afterCount": 80,
|
||||
"missing": 0,
|
||||
"dmScopeRetired": 0,
|
||||
"pruned": 40,
|
||||
"capped": 0
|
||||
},
|
||||
@@ -136,6 +145,8 @@ traffic. Use `--store <path>` for explicit offline repair of a store file.
|
||||
"storePath": "/home/user/.openclaw/agents/work/sessions/sessions.json",
|
||||
"beforeCount": 18,
|
||||
"afterCount": 18,
|
||||
"missing": 0,
|
||||
"dmScopeRetired": 0,
|
||||
"pruned": 0,
|
||||
"capped": 0
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Related:
|
||||
|
||||
- Skills system: [Skills](/tools/skills)
|
||||
- Skills config: [Skills config](/tools/skills-config)
|
||||
- ClawHub installs: [ClawHub](/tools/clawhub)
|
||||
- ClawHub installs: [ClawHub](/clawhub/cli)
|
||||
|
||||
## Commands
|
||||
|
||||
|
||||
@@ -96,11 +96,6 @@ install method aligned:
|
||||
- `beta` → prefers npm dist-tag `beta`, but falls back to `latest` when beta is
|
||||
missing or older than the current stable release.
|
||||
|
||||
OpenClaw does not yet have an LTS or monthly support channel. We are working
|
||||
toward monthly support lines, but `--channel` currently accepts only
|
||||
`stable`, `beta`, and `dev`. Use `--tag <version-or-dist-tag>` for a one-off
|
||||
target when you need a specific package artifact.
|
||||
|
||||
The Gateway core auto-updater (when enabled via config) launches the CLI update path
|
||||
outside the live Gateway request handler. Control-plane `update.run` package-manager
|
||||
updates force a non-deferred, no-cooldown update restart after the package swap,
|
||||
|
||||
@@ -41,19 +41,19 @@ There are two runtime families:
|
||||
|
||||
Most confusion comes from several different surfaces sharing the Codex name:
|
||||
|
||||
| Surface | OpenClaw name/config | What it does |
|
||||
| ---------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| Native Codex app-server runtime | `openai/*` plus `agentRuntime.id: "codex"` | Runs the embedded agent turn through Codex app-server. This is the usual ChatGPT/Codex subscription setup. |
|
||||
| Codex OAuth provider route | `openai-codex/*` model refs | Uses ChatGPT/Codex subscription OAuth through the normal OpenClaw PI runner. |
|
||||
| Codex ACP adapter | `runtime: "acp"`, `agentId: "codex"` | Runs Codex through the external ACP/acpx control plane. Use only when ACP/acpx is explicitly asked. |
|
||||
| Native Codex chat-control command set | `/codex ...` | Binds, resumes, steers, stops, and inspects Codex app-server threads from chat. |
|
||||
| OpenAI Platform API route for GPT/Codex-style models | `openai/*` model refs | Uses OpenAI API-key auth unless a runtime override, such as `agentRuntime.id: "codex"`, runs the turn. |
|
||||
| Surface | OpenClaw name/config | What it does |
|
||||
| ------------------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| Native Codex app-server runtime | `openai/*` model refs | Runs OpenAI embedded agent turns through Codex app-server. This is the usual ChatGPT/Codex subscription setup. |
|
||||
| Codex OAuth auth profiles | `openai-codex` auth provider | Stores ChatGPT/Codex subscription auth that the Codex app-server harness consumes. |
|
||||
| Codex ACP adapter | `runtime: "acp"`, `agentId: "codex"` | Runs Codex through the external ACP/acpx control plane. Use only when ACP/acpx is explicitly asked. |
|
||||
| Native Codex chat-control command set | `/codex ...` | Binds, resumes, steers, stops, and inspects Codex app-server threads from chat. |
|
||||
| OpenAI Platform API route for non-agent surfaces | `openai/*` plus API-key auth | Used for direct OpenAI APIs such as images, embeddings, speech, and realtime. |
|
||||
|
||||
Those surfaces are intentionally independent. Enabling the `codex` plugin makes
|
||||
the native app-server features available; it does not rewrite
|
||||
`openai-codex/*` into `openai/*`, does not change existing sessions, and does
|
||||
not make ACP the Codex default. Selecting `openai-codex/*` means "use the Codex
|
||||
OAuth provider route" unless you separately force a runtime.
|
||||
the native app-server features available; `openclaw doctor --fix` owns legacy
|
||||
`openai-codex/*` route repair and stale session pin cleanup. Selecting
|
||||
`openai/*` for an agent model now means "run this through Codex" unless a
|
||||
non-agent OpenAI API surface is being used.
|
||||
|
||||
The common ChatGPT/Codex subscription setup uses Codex OAuth for auth, but keeps
|
||||
the model ref as `openai/*` and selects the `codex` runtime:
|
||||
@@ -63,9 +63,6 @@ the model ref as `openai/*` and selects the `codex` runtime:
|
||||
agents: {
|
||||
defaults: {
|
||||
model: "openai/gpt-5.5",
|
||||
agentRuntime: {
|
||||
id: "codex",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -88,20 +85,23 @@ This is the agent-facing decision tree:
|
||||
1. If the user asks for **Codex bind/control/thread/resume/steer/stop**, use the
|
||||
native `/codex` command surface when the bundled `codex` plugin is enabled.
|
||||
2. If the user asks for **Codex as the embedded runtime** or wants the normal
|
||||
subscription-backed Codex agent experience, use
|
||||
`openai/<model>` with `agentRuntime.id: "codex"`.
|
||||
3. If the user asks for **Codex OAuth/subscription auth on the normal OpenClaw
|
||||
runner**, use `openai-codex/<model>` and leave the runtime as PI.
|
||||
4. If the user explicitly says **ACP**, **acpx**, or **Codex ACP adapter**, use
|
||||
subscription-backed Codex agent experience, use `openai/<model>`.
|
||||
3. If the user explicitly chooses **PI for an OpenAI model**, keep the model ref
|
||||
as `openai/<model>` and set `agentRuntime.id: "pi"`. A selected
|
||||
`openai-codex` auth profile is routed internally through PI's legacy
|
||||
Codex-auth transport.
|
||||
4. If legacy config still contains **`openai-codex/*` model refs**, repair it to
|
||||
`openai/<model>` with `openclaw doctor --fix`.
|
||||
5. If the user explicitly says **ACP**, **acpx**, or **Codex ACP adapter**, use
|
||||
ACP with `runtime: "acp"` and `agentId: "codex"`.
|
||||
5. If the request is for **Claude Code, Gemini CLI, OpenCode, Cursor, Droid, or
|
||||
6. If the request is for **Claude Code, Gemini CLI, OpenCode, Cursor, Droid, or
|
||||
another external harness**, use ACP/acpx, not the native sub-agent runtime.
|
||||
|
||||
| You mean... | Use... |
|
||||
| --------------------------------------- | -------------------------------------------- |
|
||||
| Codex app-server chat/thread control | `/codex ...` from the bundled `codex` plugin |
|
||||
| Codex app-server embedded agent runtime | `agentRuntime.id: "codex"` |
|
||||
| OpenAI Codex OAuth on the PI runner | `openai-codex/*` model refs |
|
||||
| Codex app-server embedded agent runtime | `openai/*` agent model refs |
|
||||
| OpenAI Codex OAuth | `openai-codex` auth profiles |
|
||||
| Claude Code or other external harness | ACP/acpx |
|
||||
|
||||
For the OpenAI-family prefix split, see [OpenAI](/providers/openai) and
|
||||
@@ -166,17 +166,17 @@ Legacy refs such as `claude-cli/claude-opus-4-7` remain supported for
|
||||
compatibility, but new config should keep the provider/model canonical and put
|
||||
the execution backend in `agentRuntime.id`.
|
||||
|
||||
`auto` mode is intentionally conservative. Plugin runtimes can claim
|
||||
provider/model pairs they understand, but the Codex plugin does not claim the
|
||||
`openai-codex` provider in `auto` mode. That keeps
|
||||
`openai-codex/*` as the explicit PI Codex OAuth route and avoids silently
|
||||
moving subscription-auth configs onto the native app-server harness.
|
||||
`auto` mode is intentionally conservative for most providers. OpenAI agent
|
||||
models are the exception: unset runtime and `auto` both resolve to the Codex
|
||||
harness. Explicit PI runtime config remains an opt-in compatibility route for
|
||||
`openai/*` agent turns; when paired with a selected `openai-codex` auth profile,
|
||||
OpenClaw routes PI internally through the legacy Codex-auth transport while
|
||||
keeping the public model ref as `openai/*`. Stale OpenAI PI session pins without
|
||||
explicit config are repaired back to Codex.
|
||||
|
||||
If `openclaw doctor` warns that the `codex` plugin is enabled while
|
||||
`openai-codex/*` still routes through PI, treat that as a diagnosis, not a
|
||||
migration. Keep the config unchanged when PI Codex OAuth is what you want.
|
||||
Switch to `openai/<model>` plus `agentRuntime.id: "codex"` only when you want native
|
||||
Codex app-server execution.
|
||||
`openai-codex/*` remains in config, treat that as legacy route state. Run
|
||||
`openclaw doctor --fix` to rewrite it to `openai/*` with the Codex runtime.
|
||||
|
||||
## Compatibility contract
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ title: "Features"
|
||||
**Channels:**
|
||||
|
||||
- Built-in channels include Discord, Google Chat, iMessage, IRC, Signal, Slack, Telegram, WebChat, and WhatsApp
|
||||
- Bundled plugin channels include BlueBubbles as a legacy iMessage bridge, Feishu, LINE, Matrix, Mattermost, Microsoft Teams, Nextcloud Talk, Nostr, QQ Bot, Synology Chat, Tlon, Twitch, Zalo, and Zalo Personal
|
||||
- Bundled plugin channels include Feishu, LINE, Matrix, Mattermost, Microsoft Teams, Nextcloud Talk, Nostr, QQ Bot, Synology Chat, Tlon, Twitch, Zalo, and Zalo Personal
|
||||
- Optional separately installed channel plugins include Voice Call and third-party packages such as WeChat
|
||||
- Third-party channel plugins can extend the Gateway further, such as WeChat
|
||||
- Group chat support with mention-based activation
|
||||
|
||||
@@ -763,7 +763,7 @@ Concrete migration hazards to preserve:
|
||||
- Telegram silent fallback delivery must deliver the full projected payload
|
||||
array. A single-payload shortcut can drop additional fallback payloads after
|
||||
projection.
|
||||
- LINE, BlueBubbles, Zalo, Nostr, and other existing assembled/helper paths may
|
||||
- LINE, Zalo, Nostr, and other existing assembled/helper paths may
|
||||
have reply-token handling, media proxying, sent-message caches, loading/status
|
||||
cleanup, or callback-only targets. They stay on channel-owned delivery until
|
||||
those semantics are represented by the send adapter and verified by tests.
|
||||
@@ -854,30 +854,30 @@ Core policy:
|
||||
|
||||
## Channel mapping
|
||||
|
||||
| Channel | Target migration |
|
||||
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Telegram | Receive ack policy plus durable final sends. Live adapter owns send plus edit preview, stale preview final send, topics, quote-reply preview skip, media fallback, and retry-after handling. |
|
||||
| Discord | Send adapter wraps existing durable payload delivery. Live adapter owns draft edit, progress draft, media/error preview cancel, reply target preservation, and message id receipts. Audit bot-authored gateway-failure echoes in shared rooms; use an outbound registry or other native equivalent if Discord cannot carry origin metadata on normal messages. |
|
||||
| Slack | Send adapter handles normal chat posts. Live adapter chooses native stream when thread shape supports it, otherwise draft preview. Receipts preserve thread timestamps. Origin adapter maps OpenClaw gateway failures to Slack `chat.postMessage.metadata` and drops tagged bot-room echoes before `allowBots` authorization. |
|
||||
| WhatsApp | Send adapter owns text/media send with durable final intents. Receive adapter handles group mention and sender identity. Live can stay absent until WhatsApp has an editable transport. |
|
||||
| Matrix | Live adapter owns draft event edits, finalization, redaction, encrypted media constraints, and reply-target mismatch fallback. Receive adapter owns encrypted event hydration and dedupe. Origin adapter should encode OpenClaw gateway-failure origin into Matrix event content and drop configured-bot room echoes before `allowBots` handling. |
|
||||
| Mattermost | Live adapter owns one draft post, progress/tool folding, finalization in place, and fresh-send fallback. |
|
||||
| Microsoft Teams | Live adapter owns native progress and block stream behavior. Send adapter owns activities and attachment/card receipts. |
|
||||
| Feishu | Render adapter owns text/card/raw rendering. Live adapter owns streaming cards and duplicate final suppression. Send adapter owns comments, topic sessions, media, and voice suppression. |
|
||||
| QQ Bot | Live adapter owns C2C streaming, accumulator timeout, and fallback final send. Render adapter owns media tags and text-as-voice. |
|
||||
| Signal | Simple receive plus send adapter. No live adapter unless signal-cli adds reliable edit support. |
|
||||
| iMessage and BlueBubbles | Simple receive plus send adapter. iMessage send must preserve monitor echo-cache population before durable finals can bypass monitor delivery. BlueBubbles-specific typing, reactions, and attachments remain adapter capabilities. |
|
||||
| Google Chat | Simple receive plus send adapter with thread relation mapped to spaces and thread ids. Audit `allowBots=true` room behavior for tagged OpenClaw gateway-failure echoes. |
|
||||
| LINE | Simple receive plus send adapter with reply-token constraints modeled as target/relation capability. |
|
||||
| Nextcloud Talk | SDK receive bridge plus send adapter. |
|
||||
| IRC | Simple receive plus send adapter, no durable edit receipts. |
|
||||
| Nostr | Receive plus send adapter for encrypted DMs; receipts are event ids. |
|
||||
| QA Channel | Contract-test adapter for receive, send, live, retry, and recovery behavior. |
|
||||
| Synology Chat | Simple receive plus send adapter. |
|
||||
| Tlon | Send adapter must preserve model-signature rendering and participated-thread tracking before generic durable final delivery is enabled. |
|
||||
| Twitch | Simple receive plus send adapter with rate-limit classification. |
|
||||
| Zalo | Simple receive plus send adapter. |
|
||||
| Zalo Personal | Simple receive plus send adapter. |
|
||||
| Channel | Target migration |
|
||||
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Telegram | Receive ack policy plus durable final sends. Live adapter owns send plus edit preview, stale preview final send, topics, quote-reply preview skip, media fallback, and retry-after handling. |
|
||||
| Discord | Send adapter wraps existing durable payload delivery. Live adapter owns draft edit, progress draft, media/error preview cancel, reply target preservation, and message id receipts. Audit bot-authored gateway-failure echoes in shared rooms; use an outbound registry or other native equivalent if Discord cannot carry origin metadata on normal messages. |
|
||||
| Slack | Send adapter handles normal chat posts. Live adapter chooses native stream when thread shape supports it, otherwise draft preview. Receipts preserve thread timestamps. Origin adapter maps OpenClaw gateway failures to Slack `chat.postMessage.metadata` and drops tagged bot-room echoes before `allowBots` authorization. |
|
||||
| WhatsApp | Send adapter owns text/media send with durable final intents. Receive adapter handles group mention and sender identity. Live can stay absent until WhatsApp has an editable transport. |
|
||||
| Matrix | Live adapter owns draft event edits, finalization, redaction, encrypted media constraints, and reply-target mismatch fallback. Receive adapter owns encrypted event hydration and dedupe. Origin adapter should encode OpenClaw gateway-failure origin into Matrix event content and drop configured-bot room echoes before `allowBots` handling. |
|
||||
| Mattermost | Live adapter owns one draft post, progress/tool folding, finalization in place, and fresh-send fallback. |
|
||||
| Microsoft Teams | Live adapter owns native progress and block stream behavior. Send adapter owns activities and attachment/card receipts. |
|
||||
| Feishu | Render adapter owns text/card/raw rendering. Live adapter owns streaming cards and duplicate final suppression. Send adapter owns comments, topic sessions, media, and voice suppression. |
|
||||
| QQ Bot | Live adapter owns C2C streaming, accumulator timeout, and fallback final send. Render adapter owns media tags and text-as-voice. |
|
||||
| Signal | Simple receive plus send adapter. No live adapter unless signal-cli adds reliable edit support. |
|
||||
| iMessage | Simple receive plus send adapter. iMessage send must preserve monitor echo-cache population before durable finals can bypass monitor delivery. |
|
||||
| Google Chat | Simple receive plus send adapter with thread relation mapped to spaces and thread ids. Audit `allowBots=true` room behavior for tagged OpenClaw gateway-failure echoes. |
|
||||
| LINE | Simple receive plus send adapter with reply-token constraints modeled as target/relation capability. |
|
||||
| Nextcloud Talk | SDK receive bridge plus send adapter. |
|
||||
| IRC | Simple receive plus send adapter, no durable edit receipts. |
|
||||
| Nostr | Receive plus send adapter for encrypted DMs; receipts are event ids. |
|
||||
| QA Channel | Contract-test adapter for receive, send, live, retry, and recovery behavior. |
|
||||
| Synology Chat | Simple receive plus send adapter. |
|
||||
| Tlon | Send adapter must preserve model-signature rendering and participated-thread tracking before generic durable final delivery is enabled. |
|
||||
| Twitch | Simple receive plus send adapter with rate-limit classification. |
|
||||
| Zalo | Simple receive plus send adapter. |
|
||||
| Zalo Personal | Simple receive plus send adapter. |
|
||||
|
||||
## Migration plan
|
||||
|
||||
@@ -1035,7 +1035,7 @@ Channel tests:
|
||||
- Discord prepared dispatcher finals route through the send context before docs
|
||||
or changelog claim Discord final-reply durability.
|
||||
- iMessage durable final sends populate the monitor sent-message echo cache.
|
||||
- LINE, BlueBubbles, Zalo, and Nostr legacy delivery paths are not bypassed by
|
||||
- LINE, Zalo, and Nostr legacy delivery paths are not bypassed by
|
||||
generic durable send until their adapter parity tests exist.
|
||||
- Direct-DM/Nostr callback delivery remains authoritative unless explicitly
|
||||
migrated to a complete message target and replay-safe send adapter.
|
||||
|
||||
@@ -59,7 +59,7 @@ Config (global default + per-channel overrides):
|
||||
Notes:
|
||||
|
||||
- Debounce applies to **text-only** messages; media/attachments flush immediately.
|
||||
- Control commands bypass debouncing so they remain standalone — **except** when a channel explicitly opts in to same-sender DM coalescing (e.g. [BlueBubbles `coalesceSameSenderDms`](/channels/bluebubbles#coalescing-split-send-dms-command--url-in-one-composition)), where DM commands wait inside the debounce window so a split-send payload can join the same agent turn.
|
||||
- Control commands bypass debouncing so they remain standalone. Channels that explicitly opt in to same-sender DM coalescing can keep DM commands inside the debounce window so a split-send payload can join the same agent turn.
|
||||
|
||||
## Sessions and devices
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ Common channels supporting this pattern include:
|
||||
|
||||
- `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`
|
||||
- `irc`, `line`, `googlechat`, `mattermost`, `matrix`, `nextcloud-talk`
|
||||
- `bluebubbles`, `zalo`, `zalouser`, `nostr`, `feishu`
|
||||
- `zalo`, `zalouser`, `nostr`, `feishu`
|
||||
|
||||
## Concepts
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ into the final answer when the channel can do that safely.
|
||||
|
||||
```text
|
||||
Shelling...
|
||||
📖 Read: from docs/concepts/progress-drafts.md
|
||||
📖 from docs/concepts/progress-drafts.md
|
||||
🔎 Web Search: for "discord edit message"
|
||||
🛠️ Exec: run tests
|
||||
🛠️ Bash: run tests
|
||||
```
|
||||
|
||||
Use progress drafts when you want one tidy status message during tool-heavy work
|
||||
@@ -51,15 +51,17 @@ progress chatter for that turn.
|
||||
|
||||
A progress draft has two parts:
|
||||
|
||||
| Part | Purpose |
|
||||
| -------------- | --------------------------------------------------------------------------- |
|
||||
| Label | A short title such as `Thinking...` or `Shelling...`. |
|
||||
| Progress lines | Compact run updates using the same tool labels and icons as verbose output. |
|
||||
| Part | Purpose |
|
||||
| -------------- | ------------------------------------------------------------------------------------- |
|
||||
| Label | A short starter/status line such as `Thinking...` or `Shelling...`. |
|
||||
| Progress lines | Compact run updates using the same tool icons and detail formatter as verbose output. |
|
||||
|
||||
The label appears after the agent starts meaningful work and either remains busy
|
||||
for five seconds or emits a second work event. Plain text-only replies do not
|
||||
show a progress draft. Progress lines are added only when the agent emits useful
|
||||
work updates, for example `🛠️ Exec`, `🔎 Web Search`, or `✍️ Write: to /tmp/file`.
|
||||
for five seconds or emits a second work event. It is part of the rolling progress
|
||||
line list, so the starter status scrolls away once enough concrete work appears.
|
||||
Plain text-only replies do not show a progress draft. Progress lines are added
|
||||
only when the agent emits useful work updates, for example `🛠️ Bash: run tests`,
|
||||
`🔎 Web Search: for "discord edit message"`, or `✍️ Write: to /tmp/file`.
|
||||
By default they use the same compact explain mode as `/verbose`; set
|
||||
`agents.defaults.toolProgressDetail: "raw"` when debugging and you also want raw
|
||||
commands/details appended.
|
||||
@@ -189,16 +191,16 @@ OpenClaw uses the same formatter for progress drafts and `/verbose`:
|
||||
```
|
||||
|
||||
`"explain"` is the default and keeps drafts stable with concise labels like
|
||||
`🛠️ Exec: check JS syntax for /tmp/app.js`. `"raw"` appends the underlying
|
||||
`🛠️ check JS syntax for /tmp/app.js`. `"raw"` appends the underlying
|
||||
command/detail when available, which is useful while debugging but noisier in
|
||||
chat.
|
||||
|
||||
For example, the same command appears differently depending on the detail mode:
|
||||
|
||||
| Mode | Progress line |
|
||||
| --------- | -------------------------------------------------------------------- |
|
||||
| `explain` | `🛠️ Exec: check JS syntax for /tmp/app.js` |
|
||||
| `raw` | `🛠️ Exec: check JS syntax for /tmp/app.js, node --check /tmp/app.js` |
|
||||
| Mode | Progress line |
|
||||
| --------- | -------------------------------------------------------------- |
|
||||
| `explain` | `🛠️ check JS syntax for /tmp/app.js` |
|
||||
| `raw` | `🛠️ check JS syntax for /tmp/app.js, node --check /tmp/app.js` |
|
||||
|
||||
Limit how many lines stay visible:
|
||||
|
||||
|
||||
@@ -316,14 +316,24 @@ Required env when `--credential-source env`:
|
||||
Optional:
|
||||
|
||||
- `OPENCLAW_QA_DISCORD_CAPTURE_CONTENT=1` keeps message bodies in observed-message artifacts.
|
||||
- `OPENCLAW_QA_DISCORD_VOICE_CHANNEL_ID` selects the voice/stage channel for `discord-voice-autojoin`; without it, the scenario picks the first visible voice/stage channel for the SUT bot.
|
||||
|
||||
Scenarios (`extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts:36`):
|
||||
|
||||
- `discord-canary`
|
||||
- `discord-mention-gating`
|
||||
- `discord-native-help-command-registration`
|
||||
- `discord-voice-autojoin` - opt-in voice scenario. Runs by itself, enables `channels.discord.voice.autoJoin`, and verifies the SUT bot's current Discord voice state is the target voice/stage channel. Convex Discord credentials may include optional `voiceChannelId`; otherwise the runner discovers the first visible voice/stage channel in the guild.
|
||||
- `discord-status-reactions-tool-only` - opt-in Mantis scenario. Runs by itself because it switches the SUT to always-on, tool-only guild replies with `messages.statusReactions.enabled=true`, then captures a REST reaction timeline plus HTML/PNG visual artifacts. Mantis before/after reports also preserve scenario-provided MP4 artifacts as `baseline.mp4` and `candidate.mp4`.
|
||||
|
||||
Run the Discord voice auto-join scenario explicitly:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa discord \
|
||||
--scenario discord-voice-autojoin \
|
||||
--provider-mode mock-openai
|
||||
```
|
||||
|
||||
Run the Mantis status-reaction scenario explicitly:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -131,6 +131,12 @@ Maintenance preserves durable external conversation pointers, including group
|
||||
sessions and thread-scoped chat sessions, while still allowing synthetic cron,
|
||||
hook, heartbeat, ACP, and sub-agent entries to age out.
|
||||
|
||||
If you previously used direct-message isolation and later returned
|
||||
`session.dmScope` to `main`, preview stale peer-keyed DM rows with
|
||||
`openclaw sessions cleanup --dry-run --fix-dm-scope`. Applying the same flag
|
||||
retires those old direct-DM rows and keeps their transcripts as deleted
|
||||
archives.
|
||||
|
||||
Preview with `openclaw sessions cleanup --dry-run`.
|
||||
|
||||
## Inspecting sessions
|
||||
|
||||
@@ -137,10 +137,9 @@ collaboration-mode instructions inside the Codex runtime after OpenClaw sends
|
||||
thread and turn params.
|
||||
|
||||
Regenerate them with `pnpm prompt:snapshots:gen` and verify drift with
|
||||
`pnpm prompt:snapshots:check`. CI runs the drift check as a dedicated
|
||||
additional check for manual CI and prompt-affecting changes so prompt changes
|
||||
and snapshot updates stay attached to the same PR without slowing unrelated
|
||||
boundary shards.
|
||||
`pnpm prompt:snapshots:check`. CI runs the drift check in the additional
|
||||
boundary shard so prompt changes and snapshot updates stay attached to the same
|
||||
PR.
|
||||
|
||||
## Workspace bootstrap injection
|
||||
|
||||
|
||||
@@ -94,8 +94,8 @@ Connect (first message):
|
||||
"id": "c1",
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 3,
|
||||
"minProtocol": 4,
|
||||
"maxProtocol": 4,
|
||||
"client": {
|
||||
"id": "openclaw-macos",
|
||||
"displayName": "macos",
|
||||
@@ -117,7 +117,7 @@ Hello-ok response:
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"type": "hello-ok",
|
||||
"protocol": 3,
|
||||
"protocol": 4,
|
||||
"server": { "version": "dev", "connId": "ws-1" },
|
||||
"features": { "methods": ["health"], "events": ["tick"] },
|
||||
"snapshot": {
|
||||
@@ -163,8 +163,8 @@ ws.on("open", () => {
|
||||
id: "c1",
|
||||
method: "connect",
|
||||
params: {
|
||||
minProtocol: 3,
|
||||
maxProtocol: 3,
|
||||
minProtocol: 4,
|
||||
maxProtocol: 4,
|
||||
client: {
|
||||
id: "cli",
|
||||
displayName: "example",
|
||||
@@ -272,7 +272,7 @@ Unknown frame types are preserved as raw payloads for forward compatibility.
|
||||
|
||||
## Versioning + compatibility
|
||||
|
||||
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema.ts`.
|
||||
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`.
|
||||
- Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches.
|
||||
- The Swift models keep unknown frame types to avoid breaking older clients.
|
||||
|
||||
|
||||
@@ -52,6 +52,10 @@
|
||||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/channels/bluebubbles",
|
||||
"destination": "/channels/imessage"
|
||||
},
|
||||
{
|
||||
"source": "/install/migrating-matrix",
|
||||
"destination": "/channels/matrix-migration"
|
||||
@@ -389,16 +393,16 @@
|
||||
"destination": "/channels/pairing"
|
||||
},
|
||||
{
|
||||
"source": "/clawhub",
|
||||
"destination": "/tools/clawhub"
|
||||
"source": "/clawdhub",
|
||||
"destination": "/clawhub"
|
||||
},
|
||||
{
|
||||
"source": "/clawdhub",
|
||||
"destination": "/tools/clawhub"
|
||||
"source": "/tools/clawhub",
|
||||
"destination": "/clawhub"
|
||||
},
|
||||
{
|
||||
"source": "/tools/clawdhub",
|
||||
"destination": "/tools/clawhub"
|
||||
"destination": "/clawhub"
|
||||
},
|
||||
{
|
||||
"source": "/configuration",
|
||||
@@ -1059,7 +1063,7 @@
|
||||
"channels/msteams",
|
||||
"channels/googlechat",
|
||||
"channels/imessage",
|
||||
"channels/bluebubbles",
|
||||
"channels/imessage-from-bluebubbles",
|
||||
"channels/matrix",
|
||||
"channels/matrix-migration",
|
||||
"channels/matrix-push-rules"
|
||||
@@ -1208,6 +1212,7 @@
|
||||
"plugins/sdk-channel-plugins",
|
||||
"plugins/sdk-channel-message",
|
||||
"plugins/sdk-provider-plugins",
|
||||
"plugins/cli-backend-plugins",
|
||||
"plugins/adding-capabilities",
|
||||
"plugins/compatibility",
|
||||
"plugins/sdk-migration"
|
||||
@@ -1238,7 +1243,6 @@
|
||||
"tools/creating-skills",
|
||||
"tools/skills-config",
|
||||
"tools/slash-commands",
|
||||
"tools/clawhub",
|
||||
"prose"
|
||||
]
|
||||
},
|
||||
@@ -1321,6 +1325,36 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "ClawHub",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Overview",
|
||||
"pages": ["clawhub/index", "clawhub/quickstart", "clawhub/how-it-works"]
|
||||
},
|
||||
{
|
||||
"group": "Using ClawHub",
|
||||
"pages": [
|
||||
"clawhub/cli",
|
||||
"clawhub/publishing",
|
||||
"clawhub/skill-format",
|
||||
"clawhub/soul-format",
|
||||
"clawhub/auth",
|
||||
"clawhub/telemetry",
|
||||
"clawhub/troubleshooting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "API and trust",
|
||||
"pages": [
|
||||
"clawhub/api",
|
||||
"clawhub/http-api",
|
||||
"clawhub/security",
|
||||
"clawhub/acceptable-usage"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Models",
|
||||
"groups": [
|
||||
|
||||
@@ -110,6 +110,8 @@ openclaw models auth paste-token --provider openrouter
|
||||
|
||||
OpenClaw expects the canonical `version` + `profiles` shape at runtime. If an older install still has a flat file such as `{ "openrouter": { "apiKey": "..." } }`, run `openclaw doctor --fix` to rewrite it as an `openrouter:default` API-key profile; doctor keeps a `.legacy-flat.*.bak` copy beside the original. Endpoint details such as `baseUrl`, `api`, model ids, headers, and timeouts belong under `models.providers.<id>` in `openclaw.json` or `models.json`, not in `auth-profiles.json`.
|
||||
|
||||
External auth routes such as Bedrock `auth: "aws-sdk"` are also not credentials. If you want a named Bedrock route, put `auth.profiles.<id>.mode: "aws-sdk"` in `openclaw.json`; do not write `type: "aws-sdk"` into `auth-profiles.json`. `openclaw doctor --fix` moves legacy AWS SDK markers from the credential store into config metadata.
|
||||
|
||||
Auth profile refs are also supported for static credentials:
|
||||
|
||||
- `api_key` credentials can use `keyRef: { source, provider, id }`
|
||||
|
||||
@@ -40,8 +40,10 @@ authoritative pin without explicit user intent or other out-of-band verification
|
||||
3. Client sends `pair-request`.
|
||||
4. Gateway waits for approval, then sends `pair-ok` and `hello-ok`.
|
||||
|
||||
Historically, `hello-ok` returned `serverName` and could include
|
||||
`canvasHostUrl`.
|
||||
Historically, `hello-ok` returned `serverName`; hosted plugin surfaces are now
|
||||
advertised through `pluginSurfaceUrls`. Canvas/A2UI uses
|
||||
`pluginSurfaceUrls.canvas`; the deprecated `canvasHostUrl` alias is not part of
|
||||
the refactored protocol.
|
||||
|
||||
## Frames
|
||||
|
||||
|
||||
@@ -23,6 +23,12 @@ If you want a full harness runtime with ACP session controls, background tasks,
|
||||
thread/conversation binding, and persistent external coding sessions, use
|
||||
[ACP Agents](/tools/acp-agents) instead. CLI backends are not ACP.
|
||||
|
||||
<Tip>
|
||||
Building a new backend plugin? Use
|
||||
[CLI backend plugins](/plugins/cli-backend-plugins). This page is for users
|
||||
configuring and operating an already registered backend.
|
||||
</Tip>
|
||||
|
||||
## Beginner-friendly quick start
|
||||
|
||||
You can use Codex CLI **without any config** (the bundled OpenAI plugin
|
||||
|
||||
@@ -387,7 +387,7 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
- `toolProgressDetail`: detail mode for `/verbose` tool summaries and progress-draft tool lines. Values: `"explain"` (default, compact human labels) or `"raw"` (append raw command/detail when available). Per-agent `agents.list[].toolProgressDetail` overrides this default.
|
||||
- `reasoningDefault`: default reasoning visibility for agents. Values: `"off"`, `"on"`, `"stream"`. Per-agent `agents.list[].reasoningDefault` overrides this default. Configured reasoning defaults are only applied for owners, authorized senders, or operator-admin gateway contexts when no per-message or session reasoning override is set.
|
||||
- `elevatedDefault`: default elevated-output level for agents. Values: `"off"`, `"on"`, `"ask"`, `"full"`. Default: `"on"`.
|
||||
- `model.primary`: format `provider/model` (e.g. `openai/gpt-5.5` for API-key access or `openai-codex/gpt-5.5` for Codex OAuth). If you omit the provider, OpenClaw tries an alias first, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider (deprecated compatibility behavior, so prefer explicit `provider/model`). If that provider no longer exposes the configured default model, OpenClaw falls back to the first configured provider/model instead of surfacing a stale removed-provider default.
|
||||
- `model.primary`: format `provider/model` (e.g. `openai/gpt-5.5` for OpenAI API-key or Codex OAuth access). If you omit the provider, OpenClaw tries an alias first, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider (deprecated compatibility behavior, so prefer explicit `provider/model`). If that provider no longer exposes the configured default model, OpenClaw falls back to the first configured provider/model instead of surfacing a stale removed-provider default.
|
||||
- `models`: the configured model catalog and allowlist for `/model`. Each entry can include `alias` (shortcut) and `params` (provider-specific, for example `temperature`, `maxTokens`, `cacheRetention`, `context1m`, `responsesServerCompaction`, `responsesCompactThreshold`, `chat_template_kwargs`, `extra_body`/`extraBody`).
|
||||
- Safe edits: use `openclaw config set agents.defaults.models '<json>' --strict-json --merge` to add entries. `config set` refuses replacements that would remove existing allowlist entries unless you pass `--replace`.
|
||||
- Provider-scoped configure/onboarding flows merge selected provider models into this map and preserve unrelated providers already configured.
|
||||
@@ -426,24 +426,24 @@ model, see [Agent runtimes](/concepts/agent-runtimes).
|
||||
- `id`: `"auto"`, `"pi"`, a registered plugin harness id, or a supported CLI backend alias. The bundled Codex plugin registers `codex`; the bundled Anthropic plugin provides the `claude-cli` CLI backend.
|
||||
- `id: "auto"` lets registered plugin harnesses claim supported turns and uses PI when no harness matches. An explicit plugin runtime such as `id: "codex"` requires that harness and fails closed if it is unavailable or fails.
|
||||
- Environment override: `OPENCLAW_AGENT_RUNTIME=<id|auto|pi>` overrides `id` for that process.
|
||||
- For Codex-only deployments, set `model: "openai/gpt-5.5"` and `agentRuntime.id: "codex"`.
|
||||
- OpenAI agent models use the Codex harness by default; `agentRuntime.id: "codex"` remains valid when you want to make that explicit.
|
||||
- For Claude CLI deployments, prefer `model: "anthropic/claude-opus-4-7"` plus `agentRuntime.id: "claude-cli"`. Legacy `claude-cli/claude-opus-4-7` model refs still work for compatibility, but new config should keep provider/model selection canonical and put the execution backend in `agentRuntime.id`.
|
||||
- Older runtime-policy keys are rewritten to `agentRuntime` by `openclaw doctor --fix`.
|
||||
- Harness choice is pinned per session id after the first embedded run. Config/env changes affect new or reset sessions, not an existing transcript. Legacy sessions with transcript history but no recorded pin are treated as PI-pinned. `/status` reports the effective runtime, for example `Runtime: OpenClaw Pi Default` or `Runtime: OpenAI Codex`.
|
||||
- Harness choice is pinned per session id after the first embedded run. Config/env changes affect new or reset sessions, not an existing transcript. Legacy OpenAI sessions with transcript history but no recorded pin use Codex; stale OpenAI PI pins can be repaired with `openclaw doctor --fix`. `/status` reports the effective runtime, for example `Runtime: OpenClaw Pi Default` or `Runtime: OpenAI Codex`.
|
||||
- This only controls text agent-turn execution. Media generation, vision, PDF, music, video, and TTS still use their provider/model settings.
|
||||
|
||||
**Built-in alias shorthands** (only apply when the model is in `agents.defaults.models`):
|
||||
|
||||
| Alias | Model |
|
||||
| ------------------- | ------------------------------------------ |
|
||||
| `opus` | `anthropic/claude-opus-4-6` |
|
||||
| `sonnet` | `anthropic/claude-sonnet-4-6` |
|
||||
| `gpt` | `openai/gpt-5.5` or `openai-codex/gpt-5.5` |
|
||||
| `gpt-mini` | `openai/gpt-5.4-mini` |
|
||||
| `gpt-nano` | `openai/gpt-5.4-nano` |
|
||||
| `gemini` | `google/gemini-3.1-pro-preview` |
|
||||
| `gemini-flash` | `google/gemini-3-flash-preview` |
|
||||
| `gemini-flash-lite` | `google/gemini-3.1-flash-lite-preview` |
|
||||
| Alias | Model |
|
||||
| ------------------- | -------------------------------------- |
|
||||
| `opus` | `anthropic/claude-opus-4-6` |
|
||||
| `sonnet` | `anthropic/claude-sonnet-4-6` |
|
||||
| `gpt` | `openai/gpt-5.5` |
|
||||
| `gpt-mini` | `openai/gpt-5.4-mini` |
|
||||
| `gpt-nano` | `openai/gpt-5.4-nano` |
|
||||
| `gemini` | `google/gemini-3.1-pro-preview` |
|
||||
| `gemini-flash` | `google/gemini-3-flash-preview` |
|
||||
| `gemini-flash-lite` | `google/gemini-3.1-flash-lite-preview` |
|
||||
|
||||
Your configured aliases always win over defaults.
|
||||
|
||||
@@ -1290,7 +1290,7 @@ Variables are case-insensitive. `{think}` is an alias for `{thinkingLevel}`.
|
||||
- Per-channel overrides: `channels.<channel>.ackReaction`, `channels.<channel>.accounts.<id>.ackReaction`.
|
||||
- Resolution order: account → channel → `messages.ackReaction` → identity fallback.
|
||||
- Scope: `group-mentions` (default), `group-all`, `direct`, `all`.
|
||||
- `removeAckAfterReply`: removes ack after reply on reaction-capable channels such as Slack, Discord, Telegram, WhatsApp, and BlueBubbles.
|
||||
- `removeAckAfterReply`: removes ack after reply on reaction-capable channels such as Slack, Discord, Telegram, WhatsApp, and iMessage.
|
||||
- `messages.statusReactions.enabled`: enables lifecycle status reactions on Slack, Discord, and Telegram.
|
||||
On Slack and Discord, unset keeps status reactions enabled when ack reactions are active.
|
||||
On Telegram, set it explicitly to `true` to enable lifecycle status reactions.
|
||||
@@ -1388,8 +1388,8 @@ Defaults for Talk mode (macOS/iOS/Android).
|
||||
provider: "openai",
|
||||
providers: {
|
||||
openai: {
|
||||
model: "gpt-realtime",
|
||||
voice: "alloy",
|
||||
model: "gpt-realtime-2",
|
||||
voice: "cedar",
|
||||
},
|
||||
},
|
||||
mode: "realtime",
|
||||
|
||||
@@ -581,32 +581,14 @@ When Mattermost native commands are enabled:
|
||||
- `channels.signal.configWrites`: allow or deny Signal-initiated config writes.
|
||||
- Optional `channels.signal.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
|
||||
### BlueBubbles
|
||||
|
||||
BlueBubbles is the legacy iMessage bridge (plugin-backed, configured under `channels.bluebubbles`). Existing setups remain supported, but new OpenClaw iMessage deployments should prefer `channels.imessage` when `imsg` can run on the Messages host.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enabled: true,
|
||||
dmPolicy: "pairing",
|
||||
// serverUrl, password, webhookPath, group controls, and advanced actions:
|
||||
// see /channels/bluebubbles
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Core key paths covered here: `channels.bluebubbles`, `channels.bluebubbles.dmPolicy`.
|
||||
- Optional `channels.bluebubbles.defaultAccount` overrides default account selection when it matches a configured account id.
|
||||
- Top-level `bindings[]` entries with `type: "acp"` can bind BlueBubbles conversations to persistent ACP sessions. Use a BlueBubbles handle or target string (`chat_id:*`, `chat_guid:*`, `chat_identifier:*`) in `match.peer.id`. Shared field semantics: [ACP Agents](/tools/acp-agents#persistent-channel-bindings).
|
||||
- Full BlueBubbles channel configuration and deprecation rationale are documented in [BlueBubbles](/channels/bluebubbles).
|
||||
|
||||
### iMessage
|
||||
|
||||
OpenClaw spawns `imsg rpc` (JSON-RPC over stdio). No daemon or port required. This is the preferred path for new OpenClaw iMessage setups when the host can grant Messages database and Automation permissions.
|
||||
|
||||
BlueBubbles is deprecated and no longer ships as a bundled OpenClaw channel. Migrate `channels.bluebubbles` configs to `channels.imessage`; third-party BlueBubbles bridges belong outside core.
|
||||
|
||||
If the Gateway is not running on the signed-in Messages Mac, keep `channels.imessage.enabled=true` and set `channels.imessage.cliPath` to an SSH wrapper that runs `imsg "$@"` on that Mac. The default local `imsg` path is macOS-only.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
|
||||
@@ -654,7 +654,7 @@ Only enable direct mutable name/email/nick matching with each channel's `dangero
|
||||
|
||||
- If you set `dmPolicy: "open"`, the matching `allowFrom` list must include `"*"`.
|
||||
- Provider IDs differ (phone numbers, user IDs, channel IDs). Use the provider docs to confirm the format.
|
||||
- Optional sections to add later: `web`, `browser`, `ui`, `discovery`, `canvasHost`, `talk`, `signal`, `imessage`.
|
||||
- Optional sections to add later: `web`, `browser`, `ui`, `discovery`, `plugins`, `talk`, `signal`, `imessage`.
|
||||
- See [Providers](/providers) and [Troubleshooting](/gateway/troubleshooting) for deeper setup notes.
|
||||
|
||||
## Related
|
||||
|
||||
@@ -198,8 +198,75 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
|
||||
- `plugins.entries.<id>.hooks.allowConversationAccess`: when `true`, trusted non-bundled plugins may read raw conversation content from typed hooks such as `llm_input`, `llm_output`, `before_model_resolve`, `before_agent_reply`, `before_agent_run`, `before_agent_finalize`, and `agent_end`.
|
||||
- `plugins.entries.<id>.subagent.allowModelOverride`: explicitly trust this plugin to request per-run `provider` and `model` overrides for background subagent runs.
|
||||
- `plugins.entries.<id>.subagent.allowedModels`: optional allowlist of canonical `provider/model` targets for trusted subagent overrides. Use `"*"` only when you intentionally want to allow any model.
|
||||
- `plugins.entries.<id>.llm.allowModelOverride`: explicitly trust this plugin to request model overrides for `api.runtime.llm.complete`.
|
||||
- `plugins.entries.<id>.llm.allowedModels`: optional allowlist of canonical `provider/model` targets for trusted plugin LLM completion overrides. Use `"*"` only when you intentionally want to allow any model.
|
||||
- `plugins.entries.<id>.llm.allowAgentIdOverride`: explicitly trust this plugin to run `api.runtime.llm.complete` against a non-default agent id.
|
||||
- `plugins.entries.<id>.config`: plugin-defined config object (validated by native OpenClaw plugin schema when available).
|
||||
- Channel plugin account/runtime settings live under `channels.<id>` and should be described by the owning plugin's manifest `channelConfigs` metadata, not by a central OpenClaw option registry.
|
||||
|
||||
### Codex harness plugin config
|
||||
|
||||
The bundled `codex` plugin owns native Codex app-server harness settings under
|
||||
`plugins.entries.codex.config`. See [Codex harness](/plugins/codex-harness) for
|
||||
the full runtime model.
|
||||
|
||||
`codexPlugins` applies only to sessions that select the native Codex harness.
|
||||
It does not enable Codex plugins for Pi, normal OpenAI provider runs, ACP
|
||||
conversation bindings, or any non-Codex harness.
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
codex: {
|
||||
enabled: true,
|
||||
config: {
|
||||
codexPlugins: {
|
||||
enabled: true,
|
||||
allow_destructive_actions: false,
|
||||
plugins: {
|
||||
"google-calendar": {
|
||||
enabled: true,
|
||||
marketplaceName: "openai-curated",
|
||||
pluginName: "google-calendar",
|
||||
allow_destructive_actions: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `plugins.entries.codex.config.codexPlugins.enabled`: enables native Codex
|
||||
plugin/app support for the Codex harness. Default: `false`.
|
||||
- `plugins.entries.codex.config.codexPlugins.allow_destructive_actions`:
|
||||
default destructive-action policy for migrated plugin app elicitations.
|
||||
Default: `false`.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.enabled`: enables a
|
||||
migrated plugin entry when global `codexPlugins.enabled` is also true.
|
||||
Default: `true` for explicit entries.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.marketplaceName`:
|
||||
stable marketplace identity. V1 only supports `"openai-curated"`.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.pluginName`: stable
|
||||
Codex plugin identity from migration, for example `"google-calendar"`.
|
||||
- `plugins.entries.codex.config.codexPlugins.plugins.<key>.allow_destructive_actions`:
|
||||
per-plugin destructive-action override. When omitted, the global
|
||||
`allow_destructive_actions` value is used.
|
||||
|
||||
`codexPlugins.enabled` is the global enablement directive. Explicit plugin
|
||||
entries written by migration are the durable install and repair eligibility set.
|
||||
`plugins["*"]` is not supported, there is no `install` switch, and local
|
||||
`marketplacePath` values are intentionally not config fields because they are
|
||||
host-specific.
|
||||
|
||||
`app/list` readiness checks are cached for one hour and refreshed
|
||||
asynchronously when stale. Codex thread app config is computed at Codex harness
|
||||
session establishment, not on every turn; use `/new`, `/reset`, or a gateway
|
||||
restart after changing native plugin config.
|
||||
|
||||
- `plugins.entries.firecrawl.config.webFetch`: Firecrawl web-fetch provider settings.
|
||||
- `apiKey`: Firecrawl API key (accepts SecretRef). Falls back to `plugins.entries.firecrawl.config.webSearch.apiKey`, legacy `tools.web.fetch.firecrawl.apiKey`, or `FIRECRAWL_API_KEY` env var.
|
||||
- `baseUrl`: Firecrawl API base URL (default: `https://api.firecrawl.dev`; self-hosted overrides must target private/internal endpoints).
|
||||
@@ -651,14 +718,22 @@ Validation and safety notes:
|
||||
|
||||
---
|
||||
|
||||
## Canvas host
|
||||
## Canvas plugin host
|
||||
|
||||
```json5
|
||||
{
|
||||
canvasHost: {
|
||||
root: "~/.openclaw/workspace/canvas",
|
||||
liveReload: true,
|
||||
// enabled: false, // or OPENCLAW_SKIP_CANVAS_HOST=1
|
||||
plugins: {
|
||||
entries: {
|
||||
canvas: {
|
||||
config: {
|
||||
host: {
|
||||
root: "~/.openclaw/workspace/canvas",
|
||||
liveReload: true,
|
||||
// enabled: false, // or OPENCLAW_SKIP_CANVAS_HOST=1
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
@@ -575,7 +575,7 @@ Most fields hot-apply without downtime. In `hybrid` mode, restart-required chang
|
||||
| Tools & media | `tools`, `browser`, `skills`, `mcp`, `audio`, `talk` | No |
|
||||
| UI & misc | `ui`, `logging`, `identity`, `bindings` | No |
|
||||
| Gateway server | `gateway.*` (port, bind, auth, tailscale, TLS, HTTP) | **Yes** |
|
||||
| Infrastructure | `discovery`, `canvasHost`, `plugins` | **Yes** |
|
||||
| Infrastructure | `discovery`, `plugins` | **Yes** |
|
||||
|
||||
<Note>
|
||||
`gateway.reload` and `gateway.remote` are exceptions - changing them does **not** trigger a restart.
|
||||
|
||||
@@ -264,7 +264,7 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
If you previously added legacy OpenAI transport settings under `models.providers.openai-codex`, they can shadow the built-in Codex OAuth provider path that newer releases use automatically. Doctor warns when it sees those old transport settings alongside Codex OAuth so you can remove or rewrite the stale transport override and get the built-in routing/fallback behavior back. Custom proxies and header-only overrides are still supported and do not trigger this warning.
|
||||
</Accordion>
|
||||
<Accordion title="2f. Codex route repair">
|
||||
Doctor checks for legacy `openai-codex/*` model refs. Native Codex harness routing uses canonical `openai/*` model refs plus `agentRuntime.id: "codex"` so the turn goes through the Codex app-server harness instead of the OpenClaw PI OpenAI path.
|
||||
Doctor checks for legacy `openai-codex/*` model refs. Native Codex harness routing uses canonical `openai/*` model refs; OpenAI agent turns go through the Codex app-server harness instead of the OpenClaw PI OpenAI path.
|
||||
|
||||
In `--fix` / `--repair` mode, doctor rewrites affected default-agent and per-agent refs, including primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale persisted session route state:
|
||||
|
||||
@@ -310,7 +310,6 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
- top-level payload fields (`message`, `model`, `thinking`, ...) → `payload`
|
||||
- top-level delivery fields (`deliver`, `channel`, `to`, `provider`, ...) → `delivery`
|
||||
- payload `provider` delivery aliases → explicit `delivery.channel`
|
||||
- invalid persisted cron `payload.model` sentinels (`"default"`, `"null"`, blank strings, JSON `null`) → removed model override
|
||||
- simple legacy `notify: true` webhook fallback jobs → explicit `delivery.mode="webhook"` with `delivery.to=cron.webhook`
|
||||
|
||||
Doctor only auto-migrates `notify: true` jobs when it can do so without changing behavior. If a job combines legacy notify fallback with an existing non-webhook delivery mode, doctor warns and leaves that job for manual review.
|
||||
|
||||
@@ -102,7 +102,7 @@ Outside heartbeats, stray `HEARTBEAT_OK` at the start/end of a message is stripp
|
||||
lightContext: false, // default: false; true keeps only HEARTBEAT.md from workspace bootstrap files
|
||||
isolatedSession: false, // default: false; true runs each heartbeat in a fresh session (no conversation history)
|
||||
skipWhenBusy: false, // default: false; true also waits for subagent/nested lanes
|
||||
target: "last", // default: none | options: last | none | <channel id> (core or plugin, e.g. "bluebubbles")
|
||||
target: "last", // default: none | options: last | none | <channel id> (core or plugin, e.g. "imessage")
|
||||
to: "+15551234567", // optional channel-specific override
|
||||
accountId: "ops-bot", // optional multi-account channel id
|
||||
prompt: "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
|
||||
|
||||
@@ -44,8 +44,8 @@ Client → Gateway:
|
||||
"id": "…",
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 3,
|
||||
"minProtocol": 4,
|
||||
"maxProtocol": 4,
|
||||
"client": {
|
||||
"id": "cli",
|
||||
"version": "1.2.3",
|
||||
@@ -80,7 +80,7 @@ Gateway → Client:
|
||||
"ok": true,
|
||||
"payload": {
|
||||
"type": "hello-ok",
|
||||
"protocol": 3,
|
||||
"protocol": 4,
|
||||
"server": { "version": "…", "connId": "…" },
|
||||
"features": { "methods": ["…"], "events": ["…"] },
|
||||
"snapshot": { "…": "…" },
|
||||
@@ -105,7 +105,15 @@ handshake failure.
|
||||
|
||||
`server`, `features`, `snapshot`, and `policy` are all required by the schema
|
||||
(`src/gateway/protocol/schema/frames.ts`). `auth` is also required and reports
|
||||
the negotiated role/scopes. `canvasHostUrl` is optional.
|
||||
the negotiated role/scopes. `pluginSurfaceUrls` is optional and maps plugin
|
||||
surface names, such as `canvas`, to scoped hosted URLs.
|
||||
|
||||
Scoped plugin surface URLs may expire. Nodes can call
|
||||
`node.pluginSurface.refresh` with `{ "surface": "canvas" }` to receive a fresh
|
||||
entry in `pluginSurfaceUrls`. The experimental Canvas plugin refactor does not
|
||||
support the deprecated `canvasHostUrl`, `canvasCapability`, or
|
||||
`node.canvas.capability.refresh` compatibility path; current native clients and
|
||||
gateways must use plugin surfaces.
|
||||
|
||||
When no device token is issued, `hello-ok.auth` reports the negotiated
|
||||
permissions without token fields:
|
||||
@@ -174,8 +182,8 @@ roles still need scopes under their own role prefix.
|
||||
"id": "…",
|
||||
"method": "connect",
|
||||
"params": {
|
||||
"minProtocol": 3,
|
||||
"maxProtocol": 3,
|
||||
"minProtocol": 4,
|
||||
"maxProtocol": 4,
|
||||
"client": {
|
||||
"id": "ios-node",
|
||||
"version": "1.2.3",
|
||||
@@ -443,7 +451,6 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `node.invoke` forwards a command to a connected node.
|
||||
- `node.invoke.result` returns the result for an invoke request.
|
||||
- `node.event` carries node-originated events back into the gateway.
|
||||
- `node.canvas.capability.refresh` refreshes scoped canvas-capability tokens.
|
||||
- `node.pending.pull` and `node.pending.ack` are the connected-node queue APIs.
|
||||
- `node.pending.enqueue` and `node.pending.drain` manage durable pending work for offline/disconnected nodes.
|
||||
|
||||
@@ -572,7 +579,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
|
||||
## Versioning
|
||||
|
||||
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/schema/protocol-schemas.ts`.
|
||||
- `PROTOCOL_VERSION` lives in `src/gateway/protocol/version.ts`.
|
||||
- Clients send `minProtocol` + `maxProtocol`; the server rejects mismatches.
|
||||
- Schemas + models are generated from TypeBox definitions:
|
||||
- `pnpm protocol:gen`
|
||||
@@ -582,11 +589,11 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
### Client constants
|
||||
|
||||
The reference client in `src/gateway/client.ts` uses these defaults. Values are
|
||||
stable across protocol v3 and are the expected baseline for third-party clients.
|
||||
stable across protocol v4 and are the expected baseline for third-party clients.
|
||||
|
||||
| Constant | Default | Source |
|
||||
| ----------------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| `PROTOCOL_VERSION` | `3` | `src/gateway/protocol/schema/protocol-schemas.ts` |
|
||||
| `PROTOCOL_VERSION` | `4` | `src/gateway/protocol/version.ts` |
|
||||
| Request timeout (per RPC) | `30_000` ms | `src/gateway/client.ts` (`requestTimeoutMs`) |
|
||||
| Preauth / connect-challenge timeout | `15_000` ms | `src/gateway/handshake-timeouts.ts` (config/env can raise the paired server/client budget) |
|
||||
| Initial reconnect backoff | `1_000` ms | `src/gateway/client.ts` (`backoffMs`) |
|
||||
|
||||
@@ -64,6 +64,7 @@ Rules of thumb:
|
||||
- `deny` always wins.
|
||||
- If `allow` is non-empty, everything else is treated as blocked.
|
||||
- Tool policy is the hard stop: `/exec` cannot override a denied `exec` tool.
|
||||
- Tool policy filters tool availability by name; it does not inspect side effects inside `exec`. If `exec` is allowed, denying `write`, `edit`, or `apply_patch` does not make shell commands read-only.
|
||||
- `/exec` only changes session defaults for authorized senders; it does not grant tool access.
|
||||
Provider tool keys accept either `provider` (e.g. `google-antigravity`) or `provider/model` (e.g. `openai/gpt-5.4`).
|
||||
|
||||
@@ -88,6 +89,7 @@ Available groups:
|
||||
- `group:runtime`: `exec`, `process`, `code_execution` (`bash` is accepted as
|
||||
an alias for `exec`)
|
||||
- `group:fs`: `read`, `write`, `edit`, `apply_patch`
|
||||
For read-only agents, deny `group:runtime` as well as mutating filesystem tools unless sandbox filesystem policy or a separate host boundary enforces the read-only constraint.
|
||||
- `group:sessions`: `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `sessions_yield`, `subagents`, `session_status`
|
||||
- `group:memory`: `memory_search`, `memory_get`
|
||||
- `group:web`: `web_search`, `x_search`, `web_fetch`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user