mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-11 00:11:53 +08:00
Compare commits
345 Commits
feat/confi
...
fix/codeql
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63803aa319 | ||
|
|
2235a13dab | ||
|
|
3989510251 | ||
|
|
e23d17da79 | ||
|
|
d8ed49f651 | ||
|
|
f0fa35082b | ||
|
|
4fbc490fca | ||
|
|
23fbdc1ec2 | ||
|
|
09e60e496b | ||
|
|
78e0976f93 | ||
|
|
802a73a382 | ||
|
|
10763781fd | ||
|
|
a0ca546997 | ||
|
|
476bb38527 | ||
|
|
72d8600eb5 | ||
|
|
6855b33255 | ||
|
|
bc24b547d0 | ||
|
|
0796a888ae | ||
|
|
9b91040053 | ||
|
|
90cd9fce85 | ||
|
|
a44a3f9171 | ||
|
|
bbd9702077 | ||
|
|
6afac5208a | ||
|
|
c14d2b0c1f | ||
|
|
2d9a0d9cf0 | ||
|
|
69e7e499b1 | ||
|
|
690046637f | ||
|
|
9b4f0779ce | ||
|
|
6a688e33f6 | ||
|
|
0e1f53f020 | ||
|
|
d65f28f962 | ||
|
|
e4199379ff | ||
|
|
94316334fe | ||
|
|
a6d9926d1d | ||
|
|
9123c8158d | ||
|
|
0f343ad568 | ||
|
|
04e08cea62 | ||
|
|
0ca952cdd5 | ||
|
|
1bc9bada65 | ||
|
|
ec56dd3116 | ||
|
|
5469740170 | ||
|
|
105785a1be | ||
|
|
e3be66ddda | ||
|
|
75a8f5863c | ||
|
|
526fd9d545 | ||
|
|
d74f897c1c | ||
|
|
839e7c98ff | ||
|
|
e40157013f | ||
|
|
c7b336d83e | ||
|
|
8ed52c1463 | ||
|
|
29463b9c47 | ||
|
|
2495585a32 | ||
|
|
25ecb2895a | ||
|
|
4e3b860e60 | ||
|
|
a932a58e87 | ||
|
|
566d2d73a3 | ||
|
|
1cce439c9c | ||
|
|
e989f3c868 | ||
|
|
a35d259719 | ||
|
|
8c3b1366ce | ||
|
|
d513dc7146 | ||
|
|
c43ce254e1 | ||
|
|
00d2fbfda4 | ||
|
|
e309fd485e | ||
|
|
0731fc1942 | ||
|
|
371b69b3e2 | ||
|
|
264d6f6aef | ||
|
|
921ffad7c7 | ||
|
|
87142b5fb1 | ||
|
|
57f05128cb | ||
|
|
5404bbbb71 | ||
|
|
099d18f432 | ||
|
|
1fe0e6fc4a | ||
|
|
2f6615d2ee | ||
|
|
5b80d0c15e | ||
|
|
753ccf615c | ||
|
|
5bb78ea7ed | ||
|
|
94ceb2bbe9 | ||
|
|
140ac29172 | ||
|
|
5edfbca6e5 | ||
|
|
78cfd2a512 | ||
|
|
81c2a1de26 | ||
|
|
650dc59b6f | ||
|
|
b565e6e963 | ||
|
|
e7c131d6de | ||
|
|
41282fcb13 | ||
|
|
e6ee4d6e68 | ||
|
|
f3accc753c | ||
|
|
727e0e013e | ||
|
|
be1d656514 | ||
|
|
ca0232ff0e | ||
|
|
3a4325b285 | ||
|
|
6ed642a86d | ||
|
|
569d489383 | ||
|
|
babbad81a9 | ||
|
|
1848d0dd38 | ||
|
|
194c26bcd2 | ||
|
|
14e2760835 | ||
|
|
0a41fc3ef8 | ||
|
|
dcf7f8f44c | ||
|
|
1d141c39a9 | ||
|
|
df7348e586 | ||
|
|
ebbefd6903 | ||
|
|
b018272fa1 | ||
|
|
56f4264f1b | ||
|
|
c79399dc68 | ||
|
|
9e086d6ed8 | ||
|
|
57c4279c4a | ||
|
|
37ce39b5c5 | ||
|
|
d0dafd9dca | ||
|
|
c19f8a5223 | ||
|
|
f8123e4b68 | ||
|
|
8e12c24d17 | ||
|
|
77d04a39d8 | ||
|
|
e918e5f75c | ||
|
|
edb618c6c4 | ||
|
|
fc334cda13 | ||
|
|
7741dbb759 | ||
|
|
a1090b6043 | ||
|
|
12c16576cd | ||
|
|
d228463120 | ||
|
|
435be06cde | ||
|
|
41b27024bb | ||
|
|
d74b6359fd | ||
|
|
28497515fe | ||
|
|
73cacebac3 | ||
|
|
c2ea0ce5a9 | ||
|
|
1c6911c01f | ||
|
|
956cb1c7db | ||
|
|
3f90005e56 | ||
|
|
6b0c72bec8 | ||
|
|
2c35a6e599 | ||
|
|
114c9a2f3e | ||
|
|
76a0abc768 | ||
|
|
496d90c3b5 | ||
|
|
1531123d35 | ||
|
|
b1b29a8fc2 | ||
|
|
e4bfc8066e | ||
|
|
e640c0a95f | ||
|
|
91adb69c57 | ||
|
|
8f78932059 | ||
|
|
81a41fe5be | ||
|
|
309f7f1873 | ||
|
|
cf303b3101 | ||
|
|
8d08e86f42 | ||
|
|
bd796d1c85 | ||
|
|
be51c98c5d | ||
|
|
ce364121aa | ||
|
|
f1b1c3dc99 | ||
|
|
d5166718bc | ||
|
|
cbe5515b70 | ||
|
|
1dfa52d071 | ||
|
|
f62a054ef1 | ||
|
|
265b97bbba | ||
|
|
9a2dfe0c7e | ||
|
|
f731e3754c | ||
|
|
ce884a8dae | ||
|
|
b721f1dbad | ||
|
|
0bcb4c95c1 | ||
|
|
167588cb4f | ||
|
|
9d22061e3e | ||
|
|
8a731c1ef7 | ||
|
|
969f8bfd9f | ||
|
|
289ed9830a | ||
|
|
ea4da7dfcc | ||
|
|
8f1a214a23 | ||
|
|
cbfc0ddfd1 | ||
|
|
7d343b0b10 | ||
|
|
20223e02d9 | ||
|
|
0f58a6597d | ||
|
|
8e83e52213 | ||
|
|
fbefbf05bd | ||
|
|
7f5789575e | ||
|
|
a1cb8d50ba | ||
|
|
bf7d156bb0 | ||
|
|
4a72e1b990 | ||
|
|
1841dd9977 | ||
|
|
ca1a6e29cb | ||
|
|
4038f734f7 | ||
|
|
a97fe41a9e | ||
|
|
f92a8ae9f3 | ||
|
|
2febe72108 | ||
|
|
63fac653ed | ||
|
|
d6a179bcd9 | ||
|
|
cdcc457d2e | ||
|
|
74059aaa29 | ||
|
|
9e9e024188 | ||
|
|
23a818fa2d | ||
|
|
70d1871db7 | ||
|
|
90218364b4 | ||
|
|
9d2254be06 | ||
|
|
17a213f080 | ||
|
|
bf672d1f2c | ||
|
|
b49d499b45 | ||
|
|
dcfd5913fd | ||
|
|
c3a3ceefbe | ||
|
|
34fb96622e | ||
|
|
e2fd3dcee9 | ||
|
|
d5b6667823 | ||
|
|
a8e25d9307 | ||
|
|
607bc53ff3 | ||
|
|
6a7b76e119 | ||
|
|
20c3177281 | ||
|
|
07796c9fb5 | ||
|
|
4069c81b15 | ||
|
|
afabbc01b2 | ||
|
|
b40df76c18 | ||
|
|
02f3e9cfa2 | ||
|
|
8fb24ac3ce | ||
|
|
cab66c5556 | ||
|
|
6e1017d88a | ||
|
|
89c52988c5 | ||
|
|
b64bfc5d9a | ||
|
|
1d49b8cdaa | ||
|
|
d2046beb40 | ||
|
|
958146bbac | ||
|
|
793b58b3f1 | ||
|
|
5c3eecfea7 | ||
|
|
fb7b798f96 | ||
|
|
346a72ddb9 | ||
|
|
84f183b7ad | ||
|
|
8f49c59d6d | ||
|
|
b6af40f1f1 | ||
|
|
a5f5608d06 | ||
|
|
3593beee81 | ||
|
|
a5a438a17c | ||
|
|
1915b29a3c | ||
|
|
bb6cf75463 | ||
|
|
5fe06f3cdc | ||
|
|
9d764ea075 | ||
|
|
64582bb3a7 | ||
|
|
d4971aad2c | ||
|
|
30325f567c | ||
|
|
b732f21a86 | ||
|
|
44648440a5 | ||
|
|
75d64cd4b8 | ||
|
|
03fd7df929 | ||
|
|
d757396785 | ||
|
|
7436e395d5 | ||
|
|
f34513ac66 | ||
|
|
5815ca93d9 | ||
|
|
86d897cfaa | ||
|
|
791ad0864a | ||
|
|
47a63f7acf | ||
|
|
e6ab61762a | ||
|
|
1e7ae07772 | ||
|
|
d9486c683b | ||
|
|
17401e31de | ||
|
|
0e1ef93e84 | ||
|
|
7d58362f3f | ||
|
|
5671fdca87 | ||
|
|
5eab16e086 | ||
|
|
e36b77c13e | ||
|
|
d68574653e | ||
|
|
8170df9127 | ||
|
|
b66f01bdca | ||
|
|
cd7a8f870b | ||
|
|
bb2b68b34e | ||
|
|
e9d9726f2d | ||
|
|
a018db771d | ||
|
|
690c98ad99 | ||
|
|
c410e48382 | ||
|
|
bbc0884e23 | ||
|
|
9bd348fdec | ||
|
|
dc19069d71 | ||
|
|
81307fc11d | ||
|
|
599ae7fed8 | ||
|
|
fecf1e9b8f | ||
|
|
4c0e9a4b2e | ||
|
|
cd8cb8254a | ||
|
|
2055e6ceba | ||
|
|
8ea3099cd3 | ||
|
|
e4f544790c | ||
|
|
02639d3ec8 | ||
|
|
14c9cfb637 | ||
|
|
9e9aa4722a | ||
|
|
d2ab6b4fd5 | ||
|
|
63241bf1e0 | ||
|
|
888448facc | ||
|
|
e473577eaa | ||
|
|
f204f0c999 | ||
|
|
7bbd47349e | ||
|
|
73706ca244 | ||
|
|
de0097a23c | ||
|
|
0bf4876add | ||
|
|
a00c225899 | ||
|
|
e1495c3372 | ||
|
|
75fcb8c56d | ||
|
|
31456e3326 | ||
|
|
b8a41739d5 | ||
|
|
1380dc170e | ||
|
|
d6ef1fcf24 | ||
|
|
830bd2e236 | ||
|
|
fd3840cb00 | ||
|
|
c3bfd328ad | ||
|
|
930d81aa41 | ||
|
|
ff172f46a5 | ||
|
|
afd6b5d6fc | ||
|
|
275c128e99 | ||
|
|
9ffe764416 | ||
|
|
617e1dd6bf | ||
|
|
d623354a0e | ||
|
|
44114328b4 | ||
|
|
2e0ae56b1a | ||
|
|
cd6c64d2ee | ||
|
|
649a645492 | ||
|
|
39488dfd68 | ||
|
|
8c93745f0f | ||
|
|
f56bf63b06 | ||
|
|
61b3c04424 | ||
|
|
3ec92dfac0 | ||
|
|
4324855a9d | ||
|
|
fd8a8789d0 | ||
|
|
2f622acec6 | ||
|
|
f14aa65bcc | ||
|
|
29988335fc | ||
|
|
674d188153 | ||
|
|
feb8d3a4bd | ||
|
|
5677a26385 | ||
|
|
5859dcd298 | ||
|
|
caf25fac91 | ||
|
|
521e75dea0 | ||
|
|
a7de722f4f | ||
|
|
5f4bc6ec02 | ||
|
|
f545872cbc | ||
|
|
847c00d409 | ||
|
|
88df8fe09d | ||
|
|
0bbb0eb735 | ||
|
|
80739731dd | ||
|
|
4b5c2f9aa3 | ||
|
|
dcdf97685b | ||
|
|
8e7d382c37 | ||
|
|
67506ac2a9 | ||
|
|
768bbc7cc0 | ||
|
|
390be8138f | ||
|
|
0d274ef6c2 | ||
|
|
6b3e4b88d6 | ||
|
|
39343088ed | ||
|
|
f3ba962fd0 | ||
|
|
e27e29c66e | ||
|
|
60f9358348 | ||
|
|
dc7c703425 | ||
|
|
8bead989da | ||
|
|
8659495384 | ||
|
|
c65aa1d2a6 |
@@ -35,6 +35,21 @@ Use this skill for maintainer-facing GitHub workflow, not for ordinary code chan
|
||||
- If the claim is unsubstantiated or likely wrong, request evidence or changes instead of merging.
|
||||
- If the linked issue appears outdated or incorrect, correct triage first. Do not merge a speculative fix.
|
||||
|
||||
## Close low-signal manual PRs carefully
|
||||
|
||||
- Do not close for red CI alone. Require a clear low-signal category plus stale or failed validation.
|
||||
- Good manual-close categories:
|
||||
- blank or mostly untouched PR template with no concrete OpenClaw problem/fix
|
||||
- random docs-only churn such as root README translations, generic wording tweaks, or community-plugin discoverability docs that should go through ClawHub
|
||||
- test-only coverage without a linked bug, owner request, or behavior change
|
||||
- refactor-only cleanup, variable renames, formatting, or generated/baseline churn without maintainer request
|
||||
- third-party channel/provider/tool/skill/plugin work that belongs on ClawHub instead of core
|
||||
- risky ops/infra drive-bys such as new external CI services, release workflows, host upgrade scripts, Docker base migrations, or apt retry/fix-missing tweaks without owner request and green validation
|
||||
- dirty branches where a narrow stated change includes unrelated docs/generated/runtime/extension files
|
||||
- repeated bot-review spam or copied bot output without author-owned fixes
|
||||
- Keep or escalate plausible focused bug fixes, green PRs, active maintainer discussions, assigned work, recent author follow-up, and unique reproduction details.
|
||||
- For third-party capabilities, prefer the `r: third-party-extension` auto-response label when it applies; it points contributors to publish on ClawHub.
|
||||
|
||||
## Handle GitHub text safely
|
||||
|
||||
- For issue comments and PR comments, use literal multiline strings or `-F - <<'EOF'` for real newlines. Never embed `\n`.
|
||||
|
||||
@@ -49,6 +49,19 @@ pnpm openclaw qa suite \
|
||||
5. If the user wants to watch the live UI, find the current `openclaw-qa` listen port and report `http://127.0.0.1:<port>`.
|
||||
6. If a scenario fails, fix the product or harness root cause, then rerun the full lane.
|
||||
|
||||
## OTEL smoke
|
||||
|
||||
For local QA-lab OpenTelemetry validation, use:
|
||||
|
||||
```bash
|
||||
pnpm qa:otel:smoke
|
||||
```
|
||||
|
||||
This starts a local OTLP/HTTP trace receiver, runs the `otel-trace-smoke`
|
||||
scenario through qa-channel, decodes the emitted protobuf spans, and verifies
|
||||
the exported trace names and privacy contract. It does not require Opik,
|
||||
Langfuse, or external collector credentials.
|
||||
|
||||
## QA credentials and 1Password
|
||||
|
||||
- Use `op` only inside `tmux` for QA secret lookup in this repo.
|
||||
|
||||
@@ -202,10 +202,16 @@ Before tagging or publishing, run:
|
||||
pnpm check:architecture
|
||||
pnpm build
|
||||
pnpm ui:build
|
||||
pnpm qa:otel:smoke
|
||||
pnpm release:check
|
||||
pnpm test:install:smoke
|
||||
```
|
||||
|
||||
- Use `pnpm qa:otel:smoke` when release validation needs telemetry coverage.
|
||||
It starts a local OTLP/HTTP trace receiver, runs QA-lab's
|
||||
`otel-trace-smoke`, and checks span names plus content/identifier redaction
|
||||
without external Opik or Langfuse credentials.
|
||||
|
||||
For a non-root smoke path:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -82,4 +82,5 @@ OPENCLAW_GATEWAY_TOKEN=
|
||||
|
||||
# ELEVENLABS_API_KEY=...
|
||||
# XI_API_KEY=... # alias for ElevenLabs
|
||||
# INWORLD_API_KEY=...
|
||||
# DEEPGRAM_API_KEY=...
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
name: openclaw-codeql-javascript-typescript
|
||||
name: openclaw-codeql-javascript-typescript-core
|
||||
|
||||
paths:
|
||||
- src
|
||||
- extensions
|
||||
- ui/src
|
||||
- skills
|
||||
|
||||
18
.github/codeql/codeql-javascript-typescript-extensions.yml
vendored
Normal file
18
.github/codeql/codeql-javascript-typescript-extensions.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: openclaw-codeql-javascript-typescript-extensions
|
||||
|
||||
paths:
|
||||
- extensions
|
||||
|
||||
paths-ignore:
|
||||
- apps
|
||||
- dist
|
||||
- docs
|
||||
- "**/node_modules"
|
||||
- "**/coverage"
|
||||
- "**/*.generated.ts"
|
||||
- "**/*.bundle.js"
|
||||
- "**/*-runtime.js"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/*.e2e.test.ts"
|
||||
- "**/*.e2e.test.tsx"
|
||||
16
.github/labeler.yml
vendored
16
.github/labeler.yml
vendored
@@ -3,6 +3,12 @@
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/bluebubbles/**"
|
||||
- "docs/channels/bluebubbles.md"
|
||||
"plugin: azure-speech":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/azure-speech/**"
|
||||
- "docs/providers/azure-speech.md"
|
||||
- "docs/tools/tts.md"
|
||||
"channel: discord":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -307,6 +313,11 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/huggingface/**"
|
||||
"extensions: inworld":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/inworld/**"
|
||||
- "docs/providers/inworld.md"
|
||||
"extensions: kilocode":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -315,6 +326,11 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/lmstudio/**"
|
||||
"extensions: litellm":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/litellm/**"
|
||||
- "docs/providers/litellm.md"
|
||||
"extensions: openai":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
505
.github/workflows/auto-response.yml
vendored
505
.github/workflows/auto-response.yml
vendored
@@ -5,8 +5,8 @@ on:
|
||||
types: [opened, edited, labeled]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned label automation; no untrusted checkout or code execution
|
||||
types: [labeled]
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned label automation; trusted base checkout only, no untrusted PR code execution
|
||||
types: [opened, edited, synchronize, reopened, labeled]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
@@ -20,10 +20,15 @@ permissions: {}
|
||||
jobs:
|
||||
auto-response:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
- uses: actions/create-github-app-token@v3
|
||||
id: app-token
|
||||
continue-on-error: true
|
||||
@@ -36,499 +41,15 @@ jobs:
|
||||
with:
|
||||
app-id: "2971289"
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_FALLBACK }}
|
||||
- name: Handle labeled items
|
||||
- name: Run Barnacle auto-response
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ steps.app-token.outputs.token || steps.app-token-fallback.outputs.token }}
|
||||
script: |
|
||||
// Labels prefixed with "r:" are auto-response triggers.
|
||||
const activePrLimit = 10;
|
||||
const rules = [
|
||||
{
|
||||
label: "r: skill",
|
||||
close: true,
|
||||
message:
|
||||
"Thanks for the contribution! New skills should be published to [Clawhub](https://clawhub.ai) for everyone to use. We’re keeping the core lean on skills, so I’m closing this out.",
|
||||
},
|
||||
{
|
||||
label: "r: support",
|
||||
close: true,
|
||||
message:
|
||||
"Please use [our support server](https://discord.gg/clawd) and ask in #help or #users-helping-users to resolve this, or follow the stuck FAQ at https://docs.openclaw.ai/help/faq#im-stuck-whats-the-fastest-way-to-get-unstuck.",
|
||||
},
|
||||
{
|
||||
label: "r: no-ci-pr",
|
||||
close: true,
|
||||
message:
|
||||
"Please don't make PRs for test failures on main.\n\n" +
|
||||
"The team is aware of those and will handle them directly on the codebase, not only fixing the tests but also investigating what the root cause is. Having to sift through test-fix-PRs (including some that have been out of date for weeks...) on top of that doesn't help. There are already way too many PRs for humans to manage; please don't make the flood worse.\n\n" +
|
||||
"Thank you.",
|
||||
},
|
||||
{
|
||||
label: "r: too-many-prs",
|
||||
close: true,
|
||||
message:
|
||||
`Closing this PR because the author has more than ${activePrLimit} active PRs in this repo. ` +
|
||||
"Please reduce the active PR queue and reopen or resubmit once it is back under the limit. You can close your own PRs to get back under the limit.",
|
||||
},
|
||||
{
|
||||
label: "r: testflight",
|
||||
close: true,
|
||||
commentTriggers: ["testflight"],
|
||||
message: "Not available, build from source.",
|
||||
},
|
||||
{
|
||||
label: "r: third-party-extension",
|
||||
close: true,
|
||||
message:
|
||||
"Please make this as a third-party plugin that you maintain yourself in your own repo. Docs: https://docs.openclaw.ai/plugin. Feel free to open a PR after to add it to our community plugins page: https://docs.openclaw.ai/plugins/community",
|
||||
},
|
||||
{
|
||||
label: "r: moltbook",
|
||||
close: true,
|
||||
lock: true,
|
||||
lockReason: "off-topic",
|
||||
commentTriggers: ["moltbook"],
|
||||
message:
|
||||
"OpenClaw is not affiliated with Moltbook, and issues related to Moltbook should not be submitted here.",
|
||||
},
|
||||
];
|
||||
|
||||
const maintainerTeam = "maintainer";
|
||||
const pingWarningMessage =
|
||||
"Please don’t spam-ping multiple maintainers at once. Be patient, or join our community Discord for help: https://discord.gg/clawd";
|
||||
const mentionRegex = /@([A-Za-z0-9-]+)/g;
|
||||
const maintainerCache = new Map();
|
||||
const normalizeLogin = (login) => login.toLowerCase();
|
||||
const bugSubtypeLabelSpecs = {
|
||||
regression: {
|
||||
color: "D93F0B",
|
||||
description: "Behavior that previously worked and now fails",
|
||||
},
|
||||
"bug:crash": {
|
||||
color: "B60205",
|
||||
description: "Process/app exits unexpectedly or hangs",
|
||||
},
|
||||
"bug:behavior": {
|
||||
color: "D73A4A",
|
||||
description: "Incorrect behavior without a crash",
|
||||
},
|
||||
};
|
||||
const bugTypeToLabel = {
|
||||
"Regression (worked before, now fails)": "regression",
|
||||
"Crash (process/app exits or hangs)": "bug:crash",
|
||||
"Behavior bug (incorrect output/state without crash)": "bug:behavior",
|
||||
};
|
||||
const bugSubtypeLabels = Object.keys(bugSubtypeLabelSpecs);
|
||||
|
||||
const extractIssueFormValue = (body, field) => {
|
||||
if (!body) {
|
||||
return "";
|
||||
}
|
||||
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(
|
||||
`(?:^|\\n)###\\s+${escapedField}\\s*\\n([\\s\\S]*?)(?=\\n###\\s+|$)`,
|
||||
"i",
|
||||
);
|
||||
const match = body.match(regex);
|
||||
if (!match) {
|
||||
return "";
|
||||
}
|
||||
for (const line of match[1].split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const ensureLabelExists = async (name, color, description) => {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name,
|
||||
color,
|
||||
description,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const syncBugSubtypeLabel = async (issue, labelSet) => {
|
||||
if (!labelSet.has("bug")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedBugType = extractIssueFormValue(issue.body ?? "", "Bug type");
|
||||
const targetLabel = bugTypeToLabel[selectedBugType];
|
||||
if (!targetLabel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetSpec = bugSubtypeLabelSpecs[targetLabel];
|
||||
await ensureLabelExists(targetLabel, targetSpec.color, targetSpec.description);
|
||||
|
||||
for (const subtypeLabel of bugSubtypeLabels) {
|
||||
if (subtypeLabel === targetLabel) {
|
||||
continue;
|
||||
}
|
||||
if (!labelSet.has(subtypeLabel)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
name: subtypeLabel,
|
||||
});
|
||||
labelSet.delete(subtypeLabel);
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!labelSet.has(targetLabel)) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: [targetLabel],
|
||||
});
|
||||
labelSet.add(targetLabel);
|
||||
}
|
||||
};
|
||||
|
||||
const isMaintainer = async (login) => {
|
||||
if (!login) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeLogin(login);
|
||||
if (maintainerCache.has(normalized)) {
|
||||
return maintainerCache.get(normalized);
|
||||
}
|
||||
let isMember = false;
|
||||
try {
|
||||
const membership = await github.rest.teams.getMembershipForUserInOrg({
|
||||
org: context.repo.owner,
|
||||
team_slug: maintainerTeam,
|
||||
username: normalized,
|
||||
});
|
||||
isMember = membership?.data?.state === "active";
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
maintainerCache.set(normalized, isMember);
|
||||
return isMember;
|
||||
};
|
||||
|
||||
const countMaintainerMentions = async (body, authorLogin) => {
|
||||
if (!body) {
|
||||
return 0;
|
||||
}
|
||||
const normalizedAuthor = authorLogin ? normalizeLogin(authorLogin) : "";
|
||||
if (normalizedAuthor && (await isMaintainer(normalizedAuthor))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const haystack = body.toLowerCase();
|
||||
const teamMention = `@${context.repo.owner.toLowerCase()}/${maintainerTeam}`;
|
||||
if (haystack.includes(teamMention)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
const mentions = new Set();
|
||||
for (const match of body.matchAll(mentionRegex)) {
|
||||
mentions.add(normalizeLogin(match[1]));
|
||||
}
|
||||
if (normalizedAuthor) {
|
||||
mentions.delete(normalizedAuthor);
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (const login of mentions) {
|
||||
if (await isMaintainer(login)) {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
const triggerLabel = "trigger-response";
|
||||
const activePrLimitLabel = "r: too-many-prs";
|
||||
const activePrLimitOverrideLabel = "r: too-many-prs-override";
|
||||
const target = context.payload.issue ?? context.payload.pull_request;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labelSet = new Set(
|
||||
(target.labels ?? [])
|
||||
.map((label) => (typeof label === "string" ? label : label?.name))
|
||||
.filter((name) => typeof name === "string"),
|
||||
const { pathToFileURL } = require("node:url");
|
||||
const moduleUrl = pathToFileURL(
|
||||
`${process.env.GITHUB_WORKSPACE}/scripts/github/barnacle-auto-response.mjs`,
|
||||
);
|
||||
const { runBarnacleAutoResponse } = await import(moduleUrl.href);
|
||||
|
||||
const issue = context.payload.issue;
|
||||
const pullRequest = context.payload.pull_request;
|
||||
const comment = context.payload.comment;
|
||||
if (comment) {
|
||||
const authorLogin = comment.user?.login ?? "";
|
||||
if (comment.user?.type === "Bot" || authorLogin.endsWith("[bot]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentBody = comment.body ?? "";
|
||||
const responses = [];
|
||||
const mentionCount = await countMaintainerMentions(commentBody, authorLogin);
|
||||
if (mentionCount >= 3) {
|
||||
responses.push(pingWarningMessage);
|
||||
}
|
||||
|
||||
const commentHaystack = commentBody.toLowerCase();
|
||||
const commentRule = rules.find((item) =>
|
||||
(item.commentTriggers ?? []).some((trigger) =>
|
||||
commentHaystack.includes(trigger),
|
||||
),
|
||||
);
|
||||
if (commentRule) {
|
||||
responses.push(commentRule.message);
|
||||
}
|
||||
|
||||
if (responses.length > 0) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: target.number,
|
||||
body: responses.join("\n\n"),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (issue) {
|
||||
const action = context.payload.action;
|
||||
if (action === "opened" || action === "edited") {
|
||||
const issueText = `${issue.title ?? ""}\n${issue.body ?? ""}`.trim();
|
||||
const authorLogin = issue.user?.login ?? "";
|
||||
const mentionCount = await countMaintainerMentions(
|
||||
issueText,
|
||||
authorLogin,
|
||||
);
|
||||
if (mentionCount >= 3) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body: pingWarningMessage,
|
||||
});
|
||||
}
|
||||
|
||||
await syncBugSubtypeLabel(issue, labelSet);
|
||||
}
|
||||
}
|
||||
|
||||
const hasTriggerLabel = labelSet.has(triggerLabel);
|
||||
if (hasTriggerLabel) {
|
||||
labelSet.delete(triggerLabel);
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: target.number,
|
||||
name: triggerLabel,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.status !== 404) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isLabelEvent = context.payload.action === "labeled";
|
||||
if (!hasTriggerLabel && !isLabelEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (issue) {
|
||||
const title = issue.title ?? "";
|
||||
const body = issue.body ?? "";
|
||||
const haystack = `${title}\n${body}`.toLowerCase();
|
||||
const hasMoltbookLabel = labelSet.has("r: moltbook");
|
||||
const hasTestflightLabel = labelSet.has("r: testflight");
|
||||
const hasSecurityLabel = labelSet.has("security");
|
||||
if (title.toLowerCase().includes("security") && !hasSecurityLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ["security"],
|
||||
});
|
||||
labelSet.add("security");
|
||||
}
|
||||
if (title.toLowerCase().includes("testflight") && !hasTestflightLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ["r: testflight"],
|
||||
});
|
||||
labelSet.add("r: testflight");
|
||||
}
|
||||
if (haystack.includes("moltbook") && !hasMoltbookLabel) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
labels: ["r: moltbook"],
|
||||
});
|
||||
labelSet.add("r: moltbook");
|
||||
}
|
||||
}
|
||||
|
||||
const invalidLabel = "invalid";
|
||||
const spamLabel = "r: spam";
|
||||
const dirtyLabel = "dirty";
|
||||
const badBarnacleLabel = "bad-barnacle";
|
||||
const noisyPrMessage =
|
||||
"Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.";
|
||||
|
||||
if (pullRequest) {
|
||||
if (labelSet.has(badBarnacleLabel)) {
|
||||
core.info(`Skipping PR auto-response checks for #${pullRequest.number} because ${badBarnacleLabel} is present.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (labelSet.has(dirtyLabel)) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
body: noisyPrMessage,
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
state: "closed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const labelCount = labelSet.size;
|
||||
if (labelCount > 20) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
body: noisyPrMessage,
|
||||
});
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
state: "closed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (labelSet.has(spamLabel)) {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
state: "closed",
|
||||
});
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
lock_reason: "spam",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (labelSet.has(invalidLabel)) {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pullRequest.number,
|
||||
state: "closed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (issue && labelSet.has(spamLabel)) {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned",
|
||||
});
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
lock_reason: "spam",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (issue && labelSet.has(invalidLabel)) {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
state: "closed",
|
||||
state_reason: "not_planned",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pullRequest && labelSet.has(activePrLimitOverrideLabel)) {
|
||||
labelSet.delete(activePrLimitLabel);
|
||||
}
|
||||
|
||||
const rule = rules.find((item) => labelSet.has(item.label));
|
||||
if (!rule) {
|
||||
return;
|
||||
}
|
||||
|
||||
const issueNumber = target.number;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: rule.message,
|
||||
});
|
||||
|
||||
if (rule.close) {
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
state: "closed",
|
||||
});
|
||||
}
|
||||
|
||||
if (rule.lock) {
|
||||
await github.rest.issues.lock({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
lock_reason: rule.lockReason ?? "resolved",
|
||||
});
|
||||
}
|
||||
await runBarnacleAutoResponse({ github, context, core });
|
||||
|
||||
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -1231,6 +1231,7 @@ jobs:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
|
||||
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
|
||||
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
|
||||
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
37
.github/workflows/codeql.yml
vendored
37
.github/workflows/codeql.yml
vendored
@@ -19,13 +19,14 @@ permissions:
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
name: Analyze (${{ matrix.job_name }})
|
||||
runs-on: ${{ matrix.runs_on }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: javascript-typescript
|
||||
- job_name: javascript-typescript-core
|
||||
language: javascript-typescript
|
||||
runs_on: blacksmith-32vcpu-ubuntu-2404
|
||||
needs_node: true
|
||||
needs_python: false
|
||||
@@ -33,8 +34,21 @@ jobs:
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: false
|
||||
needs_autobuild: false
|
||||
config_file: ./.github/codeql/codeql-javascript-typescript.yml
|
||||
- language: actions
|
||||
analyze_category: javascript-typescript-core
|
||||
config_file: ./.github/codeql/codeql-javascript-typescript-core.yml
|
||||
- job_name: javascript-typescript-extensions
|
||||
language: javascript-typescript
|
||||
runs_on: blacksmith-32vcpu-ubuntu-2404
|
||||
needs_node: true
|
||||
needs_python: false
|
||||
needs_java: false
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: false
|
||||
needs_autobuild: false
|
||||
analyze_category: javascript-typescript-extensions
|
||||
config_file: ./.github/codeql/codeql-javascript-typescript-extensions.yml
|
||||
- job_name: actions
|
||||
language: actions
|
||||
runs_on: blacksmith-16vcpu-ubuntu-2404
|
||||
needs_node: false
|
||||
needs_python: false
|
||||
@@ -42,8 +56,10 @@ jobs:
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: false
|
||||
needs_autobuild: false
|
||||
analyze_category: actions
|
||||
config_file: ""
|
||||
- language: python
|
||||
- job_name: python
|
||||
language: python
|
||||
runs_on: blacksmith-16vcpu-ubuntu-2404
|
||||
needs_node: false
|
||||
needs_python: true
|
||||
@@ -51,8 +67,10 @@ jobs:
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: false
|
||||
needs_autobuild: false
|
||||
analyze_category: python
|
||||
config_file: ""
|
||||
- language: java-kotlin
|
||||
- job_name: java-kotlin
|
||||
language: java-kotlin
|
||||
runs_on: blacksmith-16vcpu-ubuntu-2404
|
||||
needs_node: false
|
||||
needs_python: false
|
||||
@@ -60,8 +78,10 @@ jobs:
|
||||
needs_swift_tools: false
|
||||
needs_manual_build: true
|
||||
needs_autobuild: false
|
||||
analyze_category: java-kotlin
|
||||
config_file: ""
|
||||
- language: swift
|
||||
- job_name: swift
|
||||
language: swift
|
||||
runs_on: ${{ github.repository == 'openclaw/openclaw' && 'blacksmith-12vcpu-macos-latest' || 'macos-latest' }}
|
||||
needs_node: false
|
||||
needs_python: false
|
||||
@@ -69,6 +89,7 @@ jobs:
|
||||
needs_swift_tools: true
|
||||
needs_manual_build: true
|
||||
needs_autobuild: false
|
||||
analyze_category: swift
|
||||
config_file: ""
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -135,4 +156,4 @@ jobs:
|
||||
- name: Analyze
|
||||
uses: github/codeql-action/analyze@b25d0ebf40e5b63ee81e1bd6e5d2a12b7c2aeb61 # v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
category: "/language:${{ matrix.analyze_category }}"
|
||||
|
||||
@@ -430,6 +430,11 @@ jobs:
|
||||
command: pnpm test:docker:doctor-switch
|
||||
timeout_minutes: 60
|
||||
release_path: true
|
||||
- suite_id: docker-session-runtime-context
|
||||
label: Session Runtime Context Docker E2E
|
||||
command: pnpm test:docker:session-runtime-context
|
||||
timeout_minutes: 60
|
||||
release_path: true
|
||||
- suite_id: docker-qr
|
||||
label: QR Import Docker E2E
|
||||
command: pnpm test:docker:qr
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -146,6 +146,7 @@ changelog/fragments/
|
||||
|
||||
# Local scratch workspace
|
||||
.tmp/
|
||||
.vmux*
|
||||
.artifacts/
|
||||
test/fixtures/openclaw-vitest-unit-report.json
|
||||
analysis/
|
||||
|
||||
@@ -9,6 +9,7 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
- Run docs list first: `pnpm docs:list` if available; read relevant docs only.
|
||||
- High-confidence answers only when fixing/triaging: verify source, tests, shipped/current behavior, and dependency contracts before deciding.
|
||||
- Dependency-backed behavior: read upstream dependency docs/source/types first. Do not assume APIs, defaults, errors, timing, or runtime behavior.
|
||||
- Live-verify when feasible. Check env/`~/.profile` for keys before assuming live tests are blocked; keep secret output redacted.
|
||||
- Missing deps: `pnpm install`, retry once, then report first actionable error.
|
||||
- CODEOWNERS: maint/refactor/tests ok. Larger behavior/product/security/ownership: owner ask/review.
|
||||
- Wording: product/docs/UI/changelog say "plugin/plugins"; `extensions/` is internal.
|
||||
@@ -56,7 +57,9 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
## GitHub / CI
|
||||
|
||||
- Triage: list first, hydrate few. Use bounded `gh --json --jq`; avoid repeated full comment scans.
|
||||
- Automatic PR/issue discovery: skip maintainer-owned items unless directly relevant. Do not comment, close, label, retitle, rebase, fix up, or land them without Peter asking.
|
||||
- Search/dedupe: prefer `gh search issues 'repo:openclaw/openclaw is:open <terms>' --json number,title,state,updatedAt --limit 20`.
|
||||
- GitHub search boolean text is fussy. If `OR` queries return empty, split exact terms and search title/body/comments separately before concluding no hits.
|
||||
- PR shortlist: `gh pr list ...`; then `gh pr view <n> --json number,title,body,closingIssuesReferences,files,statusCheckRollup,reviewDecision`.
|
||||
- After landing PR: search duplicate open issues/PRs. Before closing: comment why + canonical link.
|
||||
- GH comments with markdown backticks, `$`, or shell snippets: avoid inline double-quoted `--body`; use single quotes or `--body-file`.
|
||||
@@ -121,7 +124,8 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
|
||||
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
|
||||
- Changelog user-facing only; pure test/internal usually no entry.
|
||||
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s).
|
||||
- Changelog placement: active version `### Changes`/`### Fixes`; every added entry must include at least one `Thanks @author` attribution, using credited GitHub username(s). Never add `Thanks @steipete`.
|
||||
- Changelog bullets are always single-line. No wrapping/continuation across multiple lines. Long entries stay on one long line so dedupe, PR-ref, and credit-audit tooling work and so the visual style stays uniform.
|
||||
|
||||
## Git
|
||||
|
||||
|
||||
3022
CHANGELOG.md
3022
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
166
appcast.xml
166
appcast.xml
@@ -2,6 +2,54 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>OpenClaw</title>
|
||||
<item>
|
||||
<title>2026.4.24</title>
|
||||
<pubDate>Sat, 25 Apr 2026 19:34:45 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026042490</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.24</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.24</h2>
|
||||
<h3>Highlights</h3>
|
||||
<ul>
|
||||
<li>Google Meet joins OpenClaw as a bundled participant plugin, with personal Google auth, Chrome/Twilio realtime sessions, paired-node Chrome support, artifact/attendance exports, and recovery tooling for already-open Meet tabs.</li>
|
||||
<li>DeepSeek V4 Flash and V4 Pro are in the bundled catalog, V4 Flash is the onboarding default, and DeepSeek thinking/replay behavior is fixed for follow-up tool-call turns.</li>
|
||||
<li>Talk, Voice Call, and Google Meet can use realtime voice loops that consult the full OpenClaw agent for deeper tool-backed answers.</li>
|
||||
<li>Browser automation gets coordinate clicks, longer default action budgets, per-profile headless overrides, and steadier tab reuse/recovery.</li>
|
||||
<li>Plugin and model infrastructure is lighter at startup: static model catalogs, manifest-backed model rows, lazy provider dependencies, and external runtime-dependency repair for packaged installs.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Packaged installs: preserve package-root runtime dependencies and their exported subpaths when bundled plugin runtime mirrors fall back to copying shared chunks, fixing Windows npm updates that could fail to load copied <code>dist</code> modules.</li>
|
||||
<li>Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing <code>every</code> values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys.</li>
|
||||
<li>Telegram: remove the startup persisted-offset <code>getUpdates</code> preflight so polling restarts do not self-conflict before the runner starts. Fixes #69304. (#69779) Thanks @chinar-amrutkar.</li>
|
||||
<li>Browser/Playwright: ignore benign already-handled route races during guarded navigation so browser-page tasks no longer fail when Playwright tears down a route mid-flight. (#68708) Thanks @Steady-ai.</li>
|
||||
<li>Browser/aria snapshots: bind <code>format=aria</code> <code>axN</code> refs to live DOM nodes through backend DOM ids when Playwright is available, so follow-up browser actions can use those refs without timing out. (#62434) Thanks @MrKipler.</li>
|
||||
<li>Telegram: prevent duplicate in-process long pollers for the same bot token and add clearer <code>getUpdates</code> conflict diagnostics for external duplicate pollers. Fixes #56230.</li>
|
||||
<li>Browser/Linux: detect Chromium-based installs under <code>/opt/google</code>, <code>/opt/brave.com</code>, <code>/usr/lib/chromium</code>, and <code>/usr/lib/chromium-browser</code> before asking users to set <code>browser.executablePath</code>. (#48563) Thanks @lupuletic.</li>
|
||||
<li>Sessions/browser: close tracked browser tabs when idle, daily, <code>/new</code>, or <code>/reset</code> session rollover archives the previous transcript, preventing tabs from leaking past the old session. Thanks @jakozloski.</li>
|
||||
<li>Sessions/forking: fall back to transcript-estimated parent token counts when cached totals are stale or missing, so oversized thread forks start fresh instead of cloning the full parent transcript. Thanks @jalehman.</li>
|
||||
<li>OpenAI/Codex: send Codex Responses system prompts through top-level</li>
|
||||
</ul>
|
||||
<code>instructions</code> while preserving the existing native Codex payload controls.
|
||||
<ul>
|
||||
<li>MCP/CLI: retire bundled MCP runtimes at the end of one-shot <code>openclaw agent</code> and <code>openclaw infer model run</code> gateway/local executions, so repeated scripted runs do not accumulate stdio MCP child processes. Fixes #71457.</li>
|
||||
<li>OpenAI/Codex image generation: canonicalize legacy <code>openai-codex.baseUrl</code> values such as <code>https://chatgpt.com/backend-api</code> to the Codex Responses backend before calling <code>gpt-image-2</code>, matching the chat transport. Fixes #71460.</li>
|
||||
<li>Control UI: make <code>/usage</code> use the fresh context snapshot for context percentage, and include cache-write tokens in the Usage overview cache-hit denominator. Fixes #47885. Thanks @imwyvern and @Ante042.</li>
|
||||
<li>GitHub Copilot: preserve encrypted Responses reasoning item IDs during replay so Copilot can validate encrypted reasoning payloads across requests. (#71448) Thanks @a410979729-sys.</li>
|
||||
<li>Agents/replies: recover final-answer text when streamed assistant chunks contain only whitespace, preventing completed turns from surfacing as empty-payload errors. Fixes #71454. (#71467) Thanks @Sanjays2402.</li>
|
||||
<li>Feishu/TTS: transcode voice-intent MP3 and other audio replies to Ogg/Opus before sending native Feishu audio bubbles, while keeping ordinary MP3 attachments as files. Fixes #61249 and #37868.</li>
|
||||
<li>Telegram/webhook: acknowledge validated webhook updates before running bot middleware, keeping slow agent turns from tripping Telegram delivery retries while preserving per-chat processing lanes. Fixes #71392. Thanks @joelforsberg46-source.</li>
|
||||
<li>MCP: retire one-shot embedded bundled MCP runtimes at run end, skip bundle-MCP startup when a runtime tool allowlist cannot reach bundle-MCP tools, and add <code>mcp.sessionIdleTtlMs</code> idle eviction for leaked session runtimes. Fixes #71106, #71110, #70389, and #70808.</li>
|
||||
<li>MCP/config reload: hot-apply <code>mcp.*</code> changes by disposing cached session MCP runtimes, and dispose bundled MCP runtimes during gateway shutdown so removed <code>mcp.servers</code> entries reap child processes promptly. Fixes #60656.</li>
|
||||
<li>Gateway/restart continuation: durably hand restart continuations to a session-delivery queue before deleting the restart sentinel, recover queued continuation work after crashy restarts, and fall back to a session-only wake when no channel route survives reboot. (#70780) Thanks @fuller-stack-dev.</li>
|
||||
<li>Agents/tool-result pruning: harden the tool-result character estimator and context-pruning loops against malformed <code>{ type: "text" }</code> blocks created by void or undefined tool handler results, serializing non-string text payloads for size accounting so they cannot bypass trimming as zero-sized. Fixes #34979. (#51267) Thanks @cgdusek, @alvinttang, and @coffeexcoin.</li>
|
||||
<li>Daemon/service-env: add Nix Home Manager profile bin directories to generated gateway service PATHs on macOS and Linux, honoring <code>NIX_PROFILES</code> right-to-left precedence and falling back to <code>~/.nix-profile/bin</code> when unset. Fixes #44402. (#59935) Thanks @jerome-benoit.</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.24/OpenClaw-2026.4.24.zip" length="48033180" type="application/octet-stream" sparkle:edSignature="wxOfxadSZ/9iXMitaC6SA9J6YPZC3P2tkeK7HZPHzjUIlzQTvOl7EjR4aRyXzaYt1N1AK5ba+YhuCwEngrTdCQ=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.22</title>
|
||||
<pubDate>Thu, 23 Apr 2026 15:18:00 +0000</pubDate>
|
||||
@@ -315,121 +363,5 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.20/OpenClaw-2026.4.20.zip" length="47535600" type="application/octet-stream" sparkle:edSignature="D7XcNGxmc10IIayYY91RZBoascFSnXyd4dg6cSpC3+PTIwVrWYs/FwSBc/1J+1P53LlnTHKDGQYMkWVNMnRSAQ=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.15</title>
|
||||
<pubDate>Thu, 16 Apr 2026 23:33:29 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026041590</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.15</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.15</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Anthropic/models: default Anthropic selections, <code>opus</code> aliases, Claude CLI defaults, and bundled image understanding to Claude Opus 4.7.</li>
|
||||
<li>Google/TTS: add Gemini text-to-speech support to the bundled <code>google</code> plugin, including provider registration, voice selection, WAV reply output, PCM telephony output, and setup/docs guidance. (#67515) Thanks @barronlroth.</li>
|
||||
<li>Control UI/Overview: add a Model Auth status card showing OAuth token health and provider rate-limit pressure at a glance, with attention callouts when OAuth tokens are expiring or expired. Backed by a new <code>models.authStatus</code> gateway method that strips credentials and caches for 60s. (#66211) Thanks @omarshahine.</li>
|
||||
<li>Memory/LanceDB: add cloud storage support to <code>memory-lancedb</code> so durable memory indexes can run on remote object storage instead of local disk only. (#63502) Thanks @rugvedS07.</li>
|
||||
<li>GitHub Copilot/memory search: add a GitHub Copilot embedding provider for memory search, and expose a dedicated Copilot embedding host helper so plugins can reuse the transport while honoring remote overrides, token refresh, and safer payload validation. (#61718) Thanks @feiskyer and @vincentkoc.</li>
|
||||
<li>Agents/local models: add experimental <code>agents.defaults.experimental.localModelLean: true</code> to drop heavyweight default tools like <code>browser</code>, <code>cron</code>, and <code>message</code>, reducing prompt size for weaker local-model setups without changing the normal path. (#66495) Thanks @ImLukeF.</li>
|
||||
<li>Packaging/plugins: localize bundled plugin runtime deps to their owning extensions, trim the published docs payload, and tighten install/package-manager guardrails so published builds stay leaner and core stops carrying extension-owned runtime baggage. (#67099) Thanks @vincentkoc.</li>
|
||||
<li>QA/Matrix: split Matrix live QA into a source-linked <code>qa-matrix</code> runner and keep repo-private <code>qa-*</code> surfaces out of packaged and published builds. (#66723) Thanks @gumadeiras.</li>
|
||||
<li>Docs/showcase: add a scannable hero, complete section jump links, and a responsive video grid for community examples. (#48493) Thanks @jchopard69.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Gateway/tools: anchor trusted local <code>MEDIA:</code> tool-result passthrough on the exact raw name of this run's registered built-in tools, and reject client tool definitions whose names normalize-collide with a built-in or with another client tool in the same request (<code>400 invalid_request_error</code> on both JSON and SSE paths), so a client-supplied tool named like a built-in can no longer inherit its local-media trust. (#67303)</li>
|
||||
<li>Agents/replay recovery: classify the provider wording <code>401 input item ID does not belong to this connection</code> as replay-invalid, so users get the existing <code>/new</code> session reset guidance instead of a raw 401-style failure. (#66475) Thanks @dallylee.</li>
|
||||
<li>Gateway/webchat: enforce localRoots containment on webchat audio embedding path [AI-assisted]. (#67298) Thanks @pgondhi987.</li>
|
||||
<li>Matrix/pairing: block DM pairing-store entries from authorizing room control commands [AI-assisted]. (#67294) Thanks @pgondhi987.</li>
|
||||
<li>Docker/build: verify <code>@matrix-org/matrix-sdk-crypto-nodejs</code> native bindings with <code>find</code> under <code>node_modules</code> instead of a hardcoded <code>.pnpm/...</code> path so pnpm v10+ virtual-store layouts no longer fail the image build. (#67143) thanks @ly85206559.</li>
|
||||
<li>Matrix/E2EE: keep startup bootstrap conservative for passwordless token-auth bots, still attempt the guarded repair pass without requiring <code>channels.matrix.password</code>, and document the remaining password-UIA limitation. (#66228) Thanks @SARAMALI15792.</li>
|
||||
<li>Cron/announce delivery: suppress mixed-content isolated cron announce replies that end with <code>NO_REPLY</code> so trailing silent sentinels no longer leak summary text to the target channel. (#65004) thanks @neo1027144-creator.</li>
|
||||
<li>Plugins/bundled channels: partition bundled channel lazy caches by active bundled root so <code>OPENCLAW_BUNDLED_PLUGINS_DIR</code> flips stop reusing stale plugin, setup, secrets, and runtime state. (#67200) Thanks @gumadeiras.</li>
|
||||
<li>Packaging/plugins: prune common test/spec cargo from bundled plugin runtime dependencies and fail npm release validation if packaged test cargo reappears, keeping published tarballs leaner without plugin-specific special cases. (#67275) thanks @gumadeiras.</li>
|
||||
<li>Agents/context + Memory: trim default startup/skills prompt budgets, cap <code>memory_get</code> excerpts by default with explicit continuation metadata, and keep QMD reads aligned with the same bounded excerpt contract so long sessions pull less context by default without losing deterministic follow-up reads.</li>
|
||||
<li>Matrix/commands: skip DM pairing-store reads on room traffic now that room control-command authorization ignores pairing-store entries, keeping the room path narrower without changing room auth behavior. (#67325) Thanks @gumadeiras.</li>
|
||||
<li>Memory-core/dreaming: skip dreaming narrative transcripts from session-store metadata before bootstrap records land so dream diary prompt/prose lines do not pollute session ingestion. (#67315) thanks @jalehman.</li>
|
||||
<li>Agents/local models: clarify low-context preflight hints for self-hosted models, point config-backed caps at the relevant OpenClaw setting, and stop suggesting larger models when <code>agents.defaults.contextTokens</code> is the real limit. (#66236) Thanks @ImLukeF.</li>
|
||||
<li>Dreaming/memory-core: change the default <code>dreaming.storage.mode</code> from <code>inline</code> to <code>separate</code> so Dreaming phase blocks (<code>## Light Sleep</code>, <code>## REM Sleep</code>) land in <code>memory/dreaming/{phase}/YYYY-MM-DD.md</code> instead of being injected into <code>memory/YYYY-MM-DD.md</code>. Daily memory files no longer get dominated by structured candidate output, and the daily-ingestion scanner that already strips dream marker blocks no longer has to compete with hundreds of phase-block lines on every run. Operators who want the previous behavior can opt in by setting <code>plugins.entries.memory-core.config.dreaming.storage.mode: "inline"</code>. (#66412) Thanks @mjamiv.</li>
|
||||
<li>Control UI/Overview: fix false-positive "missing" alerts on the Model Auth status card for aliased providers, env-backed OAuth with auth.profiles, and unresolvable env SecretRefs. (#67253) Thanks @omarshahine.</li>
|
||||
<li>Dashboard: constrain exec approval modal overflow on desktop so long command content no longer pushes action buttons out of view. (#67082) Thanks @Ziy1-Tan.</li>
|
||||
<li>Agents/CLI transcripts: persist successful CLI-backed turns into the OpenClaw session transcript so google-gemini-cli replies appear in session history and the Control UI again. (#67490) Thanks @obviyus.</li>
|
||||
<li>Discord/tool-call text: strip standalone Gemma-style <code><function>...</function></code> tool-call payloads from visible assistant text without truncating prose examples or trailing replies. (#67318) Thanks @joelnishanth.</li>
|
||||
<li>WhatsApp/web-session: drain the pending per-auth creds save queue before reopening sockets so reconnect-time auth bootstrap no longer races in-flight <code>creds.json</code> writes and falsely restores from backup. (#67464) Thanks @neeravmakwana.</li>
|
||||
<li>BlueBubbles/catchup: add a per-message retry ceiling (<code>catchup.maxFailureRetries</code>, default 10) so a persistently-failing message with a malformed payload no longer wedges the catchup cursor forever. After N consecutive <code>processMessage</code> failures against the same GUID, catchup logs a WARN, skips that message on subsequent sweeps, and lets the cursor advance past it. Transient failures still retry from the same point as before. Also fixes a lost-update race in the persistent dedupe file lock that silently dropped inbound GUIDs on concurrent writes, a dedupe file naming migration gap on version upgrade, and a balloon-event bypass that let catchup replay debouncer-coalesced events as standalone messages. (#67426, #66870) Thanks @omarshahine.</li>
|
||||
<li>Ollama/chat: strip the <code>ollama/</code> provider prefix from Ollama chat request model ids so configured refs like <code>ollama/qwen3:14b-q8_0</code> stop 404ing against the Ollama API. (#67457) Thanks @suboss87.</li>
|
||||
<li>Agents/tools: resolve non-workspace host tilde paths against the OS home directory and keep edit recovery aligned with that same path target, so <code>~/...</code> host edit/write operations stop failing or reading back the wrong file when <code>OPENCLAW_HOME</code> differs. (#62804) Thanks @stainlu.</li>
|
||||
<li>Speech/TTS: auto-enable the bundled Microsoft and ElevenLabs speech providers, and route generic TTS directive tokens through the explicit or active provider first so overrides like <code>[[tts:speed=1.2]]</code> stop silently landing on the wrong provider. (#62846) Thanks @stainlu.</li>
|
||||
<li>OpenAI Codex/models: normalize stale native transport metadata in both runtime resolution and discovery/listing so legacy <code>openai-codex</code> rows with missing <code>api</code> or <code>https://chatgpt.com/backend-api/v1</code> self-heal to the canonical Codex transport instead of routing requests through broken HTML/Cloudflare paths, combining the original fixes proposed in #66969 (saamuelng601-pixel) and #67159 (hclsys). (#67635)</li>
|
||||
<li>Agents/failover: treat HTML provider error pages as upstream transport failures for CDN-style 5xx responses without misclassifying embedded body text as API rate limits, while still preserving auth remediation for HTML 401/403 pages and proxy remediation for HTML 407 pages. (#67642) Thanks @stainlu.</li>
|
||||
<li>Gateway/skills: bump the cached skills-snapshot version whenever a config write touches <code>skills.*</code> (for example <code>skills.allowBundled</code>, <code>skills.entries.<id>.enabled</code>, or <code>skills.profile</code>). Existing agent sessions persist a <code>skillsSnapshot</code> in <code>sessions.json</code> that reuses the skill list frozen at session creation; without this invalidation, removing a bundled skill from the allowlist left the old snapshot live and the model kept calling the disabled tool, producing <code>Tool <name> not found</code> loops that ran until the embedded-run timeout. (#67401) Thanks @xantorres.</li>
|
||||
<li>Agents/tool-loop: enable the unknown-tool stream guard by default. Previously <code>resolveUnknownToolGuardThreshold</code> returned <code>undefined</code> unless <code>tools.loopDetection.enabled</code> was explicitly set to <code>true</code>, which left the protection off in the default configuration. A hallucinated or removed tool (for example <code>himalaya</code> after it was dropped from <code>skills.allowBundled</code>) would then loop "Tool X not found" attempts until the full embedded-run timeout. The guard has no false-positive surface because it only triggers on tools that are objectively not registered in the run, so it now stays on regardless of <code>tools.loopDetection.enabled</code> and still accepts <code>tools.loopDetection.unknownToolThreshold</code> as a per-run override (default 10). (#67401) Thanks @xantorres.</li>
|
||||
<li>TUI/streaming: add a client-side streaming watchdog to <code>tui-event-handlers</code> so the <code>streaming · Xm Ys</code> activity indicator resets to <code>idle</code> after 30s of delta silence on the active run. Guards against lost or late <code>state: "final"</code> chat events (WS reconnects, gateway restarts, etc.) leaving the TUI stuck on <code>streaming</code> indefinitely; a new system log line surfaces the reset so users know to send a new message to resync. The window is configurable via the new <code>streamingWatchdogMs</code> context option (set to <code>0</code> to disable), and the handler now exposes a <code>dispose()</code> that clears the pending timer on shutdown. (#67401) Thanks @xantorres.</li>
|
||||
<li>Extensions/lmstudio: add exponential backoff to the inference-preload wrapper so an LM Studio model-load failure (for example the built-in memory guardrail rejecting a load because the swap is saturated) no longer produces a WARN line every ~2s for every chat request. The wrapper now records consecutive preload failures per <code>(baseUrl, modelKey, contextLength)</code> tuple with a 5s → 10s → 20s → … → 5min cooldown and skips the preload step entirely while a cooldown is active, letting chat requests proceed directly to the stream (the model is often already loaded via the LM Studio UI). The combined <code>preload failed</code> log line now reports consecutive-failure count and remaining cooldown so operators can act on the real issue instead of drowning in repeated warnings. (#67401) Thanks @xantorres.</li>
|
||||
<li>Agents/replay: re-run tool/result pairing after strict replay tool-call ID sanitization on outbound requests so Anthropic-compatible providers like MiniMax no longer receive malformed orphan tool-result IDs such as <code>...toolresult1</code> during compaction and retry flows. (#67620) Thanks @stainlu.</li>
|
||||
<li>Gateway/startup: fix spurious SIGUSR1 restart loop on Linux/systemd when plugin auto-enable is the only startup config write; the config hash guard was not captured for that write path, causing chokidar to treat each boot write as an external change and trigger a reload → restart cycle that corrupts manifest.db after repeated cycles. Fixes #67436. (#67557) thanks @openperf</li>
|
||||
<li>Codex/harness: auto-enable the Codex plugin when <code>codex</code> is selected as an embedded agent harness runtime, including forced default, per-agent, and <code>OPENCLAW_AGENT_RUNTIME</code> paths. (#67474) Thanks @duqaXxX.</li>
|
||||
<li>OpenAI Codex/CLI: keep resumed <code>codex exec resume</code> runs on the safe non-interactive path without reintroducing the removed dangerous bypass flag by passing the supported <code>--skip-git-repo-check</code> resume arg plus Codex's native <code>sandbox_mode="workspace-write"</code> config override. (#67666) Thanks @plgonzalezrx8.</li>
|
||||
<li>Codex/app-server: parse Desktop-originated app-server user agents such as <code>Codex Desktop/0.118.0</code>, keeping the version gate working when the Codex CLI inherits a multi-word originator. (#64666) Thanks @cyrusaf.</li>
|
||||
<li>Cron/announce delivery: keep isolated announce <code>NO_REPLY</code> stripping case-insensitive across direct and text delivery, preserve structured media-only sends when a caption strips silent, and derive main-session awareness from the cleaned payloads so silent captions no longer leak stale <code>NO_REPLY</code> text. (#65016) Thanks @BKF-Gitty.</li>
|
||||
<li>Sessions/Codex: skip redundant <code>delivery-mirror</code> transcript appends only when the latest assistant message has the same visible text, preventing duplicate visible replies on Codex-backed turns without suppressing repeated answers across turns. (#67185) Thanks @andyylin.</li>
|
||||
<li>Auto-reply/prompt-cache: keep volatile inbound chat IDs out of the stable system prompt so task-scoped adapters can reuse prompt caches across runs, while preserving conversation metadata for the user turn and media-only messages. (#65071) Thanks @MonkeyLeeT.</li>
|
||||
<li>BlueBubbles/inbound: restore inbound image attachment downloads on Node 22+ by stripping incompatible bundled-undici dispatchers from the non-SSRF fetch path, accept <code>updated-message</code> webhooks carrying attachments, use event-type-aware dedup keys so attachment follow-ups are not rejected as duplicates, and retry attachment fetch from the BB API when the initial webhook arrives with an empty array. (#64105, #61861, #65430, #67510) Thanks @omarshahine.</li>
|
||||
<li>Agents/skills: sort prompt-facing <code>available_skills</code> entries by skill name after merging sources so <code>skills.load.extraDirs</code> order no longer changes prompt-cache prefixes. (#64198) Thanks @Bartok9.</li>
|
||||
<li>Agents/OpenAI Responses: add <code>models.providers.*.models.*.compat.supportsPromptCacheKey</code> so OpenAI-compatible proxies that forward <code>prompt_cache_key</code> can keep prompt caching enabled while incompatible endpoints can still force stripping. (#67427) Thanks @damselem.</li>
|
||||
<li>Agents/context engines: keep loop-hook and final <code>afterTurn</code> prompt-cache touch metadata aligned with the current assistant turn so cache-aware context engines retain accurate cache TTL state during tool loops. (#67767) thanks @jalehman.</li>
|
||||
<li>Memory/dreaming: strip AI-facing inbound metadata envelopes from session-corpus user turns before normalization so REM topic extraction sees the user's actual message text, including array-shaped split envelopes. (#66548) Thanks @zqchris.</li>
|
||||
<li>Agents/errors: detect standalone Cloudflare/CDN HTML challenge pages before transport DNS classification so provider block pages no longer appear as local DNS lookup failures. (#67704) Thanks @chris-yyau.</li>
|
||||
<li>Security/approvals: redact secrets in exec approval prompts so inline approval review can no longer leak credential material in rendered prompt content. (#61077, #64790)</li>
|
||||
<li>CLI/configure: re-read the persisted config hash after writes so config updates stop failing with stale-hash races. (#64188, #66528)</li>
|
||||
<li>CLI/update: prune stale packaged <code>dist</code> chunks after npm upgrades and keep downgrade/verify inventory checks compat-safe so global upgrades stop failing on stale chunk imports. (#66959) Thanks @obviyus.</li>
|
||||
<li>Onboarding/CLI: fix channel-selection crashes on globally installed CLI setups during onboarding. (#66736)</li>
|
||||
<li>Video generation/live tests: bound provider polling for live video smoke, default to the fast non-FAL text-to-video path, and use a one-second lobster prompt so release validation no longer waits indefinitely on slow provider queues.</li>
|
||||
<li>Memory-core/QMD <code>memory_get</code>: reject reads of arbitrary workspace markdown paths and only allow canonical memory files (<code>MEMORY.md</code>, <code>memory.md</code>, <code>DREAMS.md</code>, <code>dreams.md</code>, <code>memory/**</code>) plus exact paths of active indexed QMD workspace documents, so the QMD memory backend can no longer be used as a generic workspace-file read shim that bypasses <code>read</code> tool-policy denials. (#66026) Thanks @eleqtrizit.</li>
|
||||
<li>Cron/agents: forward embedded-run tool policy and internal event params into the attempt layer so <code>--tools</code> allowlists, cron-owned message-tool suppression, explicit message targeting, and command-path internal events all take effect at runtime again. (#62675) Thanks @hexsprite.</li>
|
||||
<li>Setup/providers: guard preferred-provider lookup during setup so malformed plugin metadata with a missing provider id no longer crashes the wizard with <code>Cannot read properties of undefined (reading 'trim')</code>. (#66649) Thanks @Tianworld.</li>
|
||||
<li>Matrix/security: normalize sandboxed profile avatar params, preserve <code>mxc://</code> avatar URLs, and surface gmail watcher stop failures during reload. (#64701) Thanks @slepybear.</li>
|
||||
<li>Telegram/documents: drop leaked binary caption bytes from inbound Telegram text handling so document uploads like <code>.mobi</code> or <code>.epub</code> no longer explode prompt token counts. (#66663) Thanks @joelnishanth.</li>
|
||||
<li>Gateway/auth: resolve the active gateway bearer per-request on the HTTP server and the HTTP upgrade handler via <code>getResolvedAuth()</code>, mirroring the WebSocket path, so a secret rotated through <code>secrets.reload</code> or config hot-reload stops authenticating on <code>/v1/*</code>, <code>/tools/invoke</code>, plugin HTTP routes, and the canvas upgrade path immediately instead of remaining valid on HTTP until gateway restart. (#66651) Thanks @mmaps.</li>
|
||||
<li>Agents/compaction: cap the compaction reserve-token floor to the model context window so small-context local models (e.g. Ollama with 16K tokens) no longer trigger context-overflow errors or infinite compaction loops on every prompt. (#65671) Thanks @openperf.</li>
|
||||
<li>Agents/OpenAI Responses: classify the exact <code>Unknown error (no error details in response)</code> transport failure as failover reason <code>unknown</code> so assistant/model fallback still runs for that no-details failure path. (#65254) Thanks @OpenCodeEngineer.</li>
|
||||
<li>Models/probe: surface invalid-model probe failures as <code>format</code> instead of <code>unknown</code> in <code>models list --probe</code>, and lock the invalid-model fallback path in with regression coverage. (#50028) Thanks @xiwuqi.</li>
|
||||
<li>Agents/failover: classify OpenAI-compatible <code>finish_reason: network_error</code> stream failures as timeout so model fallback retries continue instead of stopping with an unknown failover reason. (#61784) thanks @lawrence3699.</li>
|
||||
<li>Onboarding/channels: normalize channel setup metadata before discovery and validation so malformed or mixed-shape channel plugin metadata no longer breaks setup and onboarding channel lists. (#66706) Thanks @darkamenosa.</li>
|
||||
<li>Slack/native commands: fix option menus for slash commands such as <code>/verbose</code> when Slack renders native buttons by giving each button a unique action ID while still routing them through the shared <code>openclaw_cmdarg*</code> listener. Thanks @Wangmerlyn.</li>
|
||||
<li>Feishu/webhook: harden the webhook transport and card-action replay guards to fail closed on missing <code>encryptKey</code> and blank callback tokens — refuse to start the webhook transport without an <code>encryptKey</code>, reject unsigned requests when no key is present instead of accepting them, and drop blank card-action tokens before the dedupe claim and dispatcher. Defense-in-depth over the already-closed monitor-account layer. (#66707) Thanks @eleqtrizit.</li>
|
||||
<li>Agents/workspace files: route <code>agents.files.get</code>, <code>agents.files.set</code>, and workspace listing through the shared <code>fs-safe</code> helpers (<code>openFileWithinRoot</code>/<code>readFileWithinRoot</code>/<code>writeFileWithinRoot</code>), reject symlink aliases for allowlisted agent files, and have <code>fs-safe</code> resolve opened-file real paths from the file descriptor before falling back to path-based <code>realpath</code> so a symlink swap between <code>open</code> and <code>realpath</code> can no longer redirect the validated path off the intended inode. (#66636) Thanks @eleqtrizit.</li>
|
||||
<li>Gateway/MCP loopback: switch the <code>/mcp</code> bearer comparison from plain <code>!==</code> to constant-time <code>safeEqualSecret</code> (matching the convention every other auth surface in the codebase uses), and reject non-loopback browser-origin requests via <code>checkBrowserOrigin</code> before the auth gate runs. Loopback origins (<code>127.0.0.1:*</code>, <code>localhost:*</code>, same-origin) still go through, including the <code>localhost</code>↔<code>127.0.0.1</code> host mismatch that browsers flag as <code>Sec-Fetch-Site: cross-site</code>. (#66665) Thanks @eleqtrizit.</li>
|
||||
<li>Auto-reply/billing: classify pure billing cooldown fallback summaries from structured fallback reasons so users see billing guidance instead of the generic failure reply. (#66363) Thanks @Rohan5commit.</li>
|
||||
<li>Agents/fallback: preserve the original prompt body on model fallback retries with session history so the retrying model keeps the active task instead of only seeing a generic continue message. (#66029) Thanks @WuKongAI-CMU.</li>
|
||||
<li>Reply/secrets: resolve active reply channel/account SecretRefs before reply-run message-action discovery so channel token SecretRefs (for example Discord) do not degrade into discovery-time unresolved-secret failures. (#66796) Thanks @joshavant.</li>
|
||||
<li>Agents/Anthropic: ignore non-positive Anthropic Messages token overrides and fail locally when no positive token budget remains, so invalid <code>max_tokens</code> values no longer reach the provider API. (#66664) thanks @jalehman</li>
|
||||
<li>Agents/context engines: preserve prompt-only token counts, not full request totals, when deferred maintenance reuses after-turn runtime context so background compaction bookkeeping matches the active prompt window. (#66820) thanks @jalehman.</li>
|
||||
<li>BlueBubbles/inbound: add a persistent file-backed GUID dedupe so MessagePoller webhook replays after BB Server restart or reconnect no longer cause the agent to re-reply to already-handled messages. (#19176, #12053, #66816) Thanks @omarshahine.</li>
|
||||
<li>Secrets/plugins/status: align SecretRef inspect-vs-strict handling across plugin preload, read-only status/agents surfaces, and runtime auth paths so unresolved refs no longer crash read-only CLI flows while runtime-required non-env refs stay strict. (#66818) Thanks @joshavant.</li>
|
||||
<li>Memory/dreaming: stop ordinary transcripts that merely quote the dream-diary prompt from being classified as internal dreaming runs and silently dropped from session recall ingestion. (#66852) Thanks @gumadeiras.</li>
|
||||
<li>Telegram/documents: sanitize binary reply context and ZIP-like archive extraction so <code>.epub</code> and <code>.mobi</code> uploads can no longer leak raw binary into prompt context through reply metadata or archive-to-<code>text/plain</code> coercion. (#66877) Thanks @martinfrancois.</li>
|
||||
<li>Telegram/native commands: restore plugin-registry-backed auto defaults for native commands and native skills so Telegram slash commands keep registering when <code>commands.native</code> and <code>commands.nativeSkills</code> stay on <code>auto</code>. (#66843) Thanks @kashevk0.</li>
|
||||
<li>OpenRouter/Qwen3: parse <code>reasoning_details</code> stream deltas as thinking content without skipping same-chunk tool calls, so Qwen3 replies no longer fail empty on OpenRouter and mixed reasoning/tool-call chunks still execute normally. (#66905) Thanks @bladin.</li>
|
||||
<li>BlueBubbles/catchup: replay missed webhook messages after gateway restart via a persistent per-account cursor and <code>/api/v1/message/query?after=<ts></code> pass, so messages delivered while the gateway was down no longer disappear. Uses the existing <code>processMessage</code> path and is deduped by #66816's inbound GUID cache. (#66857, #66721) Thanks @omarshahine.</li>
|
||||
<li>Telegram/native commands: keep Telegram command-sync cache process-local so gateway restarts re-register the menu instead of trusting stale on-disk sync state after Telegram cleared commands out-of-band. (#66730) Thanks @nightq.</li>
|
||||
<li>Audio/self-hosted STT: restore <code>models.providers.*.request.allowPrivateNetwork</code> for audio transcription so private or LAN speech-to-text endpoints stop tripping SSRF blocks after the v2026.4.14 regression. (#66692) Thanks @jhsmith409.</li>
|
||||
<li>Auto-reply/media: allow workspace-rooted absolute media paths in auto-reply send flows so valid local media references no longer fail path validation. (#66689)</li>
|
||||
<li>WhatsApp/Baileys media upload: harden encrypted upload handling so large outbound media sends avoid buffer spikes and reliability regressions. (#65966) Thanks @frankekn.</li>
|
||||
<li>QQBot/cron: guard against undefined <code>event.content</code> in <code>parseFaceTags</code> and <code>filterInternalMarkers</code> so cron-triggered agent turns with no content payload no longer crash with <code>TypeError: Cannot read properties of undefined (reading 'startsWith')</code>. (#66302) Thanks @xinmotlanthua.</li>
|
||||
<li>CLI/plugins: stop <code>--dangerously-force-unsafe-install</code> plugin installs from falling back to hook-pack installs after security scan failures, while still preserving non-security fallback behavior for real hook packs. (#58909) Thanks @hxy91819.</li>
|
||||
<li>Claude CLI/sessions: classify <code>No conversation found with session ID</code> as <code>session_expired</code> so expired CLI-backed conversations clear the stale binding and recover on the next turn. (#65028) thanks @Ivan-Fn.</li>
|
||||
<li>Context Engine: gracefully fall back to the legacy engine when a third-party context engine plugin fails at resolution time (unregistered id, factory throw, or contract violation), preventing a full gateway outage on every channel. (#66930) Thanks @openperf.</li>
|
||||
<li>Control UI/chat: keep optimistic user message cards visible during active sends by deferring same-session history reloads until the active run ends, including aborted and errored runs. (#66997) Thanks @scotthuang and @vincentkoc.</li>
|
||||
<li>Media/Slack: allow host-local CSV and Markdown uploads only when the fallback buffer actually decodes as text, so real plain-text files work without letting opaque non-text blobs renamed to <code>.csv</code> or <code>.md</code> slip past the host-read guard. (#67047) Thanks @Unayung.</li>
|
||||
<li>Ollama/onboarding: split setup into <code>Cloud + Local</code>, <code>Cloud only</code>, and <code>Local only</code>, support direct <code>OLLAMA_API_KEY</code> cloud setup without a local daemon, and keep Ollama web search on the local-host path. (#67005) Thanks @obviyus.</li>
|
||||
<li>Webchat/security: reject remote-host <code>file://</code> URLs in the media embedding path. (#67293) Thanks @pgondhi987.</li>
|
||||
<li>Dreaming/memory-core: use the ingestion day, not the source file day, for daily recall dedupe so repeat sweeps of the same daily note can increment <code>dailyCount</code> across days instead of stalling at <code>1</code>. (#67091) Thanks @Bartok9.</li>
|
||||
<li>Node-host/tools.exec: let approval binding distinguish known native binaries from mutable shell payload files, while still fail-closing unknown or racy file probes so absolute-path node-host commands like <code>/usr/bin/whoami</code> no longer get rejected as unsafe interpreter/runtime commands. (#66731) Thanks @tmimmanuel.</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.15/OpenClaw-2026.4.15.zip" length="47501638" type="application/octet-stream" sparkle:edSignature="JUG3cicpJqCQDvp7VYoN6qBuN4Kn4s0+QQFjlMR69OZlwViLdiStPIHa+1vpuoR4miYhJc9knSDVCFzSfQuYCQ=="/>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
</rss>
|
||||
@@ -3,6 +3,7 @@
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.NEARBY_WIFI_DEVICES"
|
||||
@@ -52,7 +53,7 @@
|
||||
<service
|
||||
android:name=".NodeForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
android:foregroundServiceType="dataSync|microphone" />
|
||||
<service
|
||||
android:name=".node.DeviceNotificationListenerService"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -101,7 +101,8 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
|
||||
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
|
||||
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
|
||||
val micEnabled: StateFlow<Boolean> = prefs.talkEnabled
|
||||
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = runtimeState(initial = VoiceCaptureMode.Off) { it.voiceCaptureMode }
|
||||
val micEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.micEnabled }
|
||||
|
||||
val micCooldown: StateFlow<Boolean> = runtimeState(initial = false) { it.micCooldown }
|
||||
val micStatusText: StateFlow<String> = runtimeState(initial = "Mic off") { it.micStatusText }
|
||||
@@ -111,6 +112,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||
val micConversation: StateFlow<List<VoiceConversationEntry>> = runtimeState(initial = emptyList()) { it.micConversation }
|
||||
val micInputLevel: StateFlow<Float> = runtimeState(initial = 0f) { it.micInputLevel }
|
||||
val micIsSending: StateFlow<Boolean> = runtimeState(initial = false) { it.micIsSending }
|
||||
val talkModeEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.talkModeEnabled }
|
||||
val talkModeListening: StateFlow<Boolean> = runtimeState(initial = false) { it.talkModeListening }
|
||||
val talkModeSpeaking: StateFlow<Boolean> = runtimeState(initial = false) { it.talkModeSpeaking }
|
||||
val talkModeStatusText: StateFlow<String> = runtimeState(initial = "Off") { it.talkModeStatusText }
|
||||
|
||||
val chatSessionKey: StateFlow<String> = runtimeState(initial = "main") { it.chatSessionKey }
|
||||
val chatSessionId: StateFlow<String?> = runtimeState(initial = null) { it.chatSessionId }
|
||||
@@ -283,6 +288,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||
ensureRuntime().setMicEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setTalkModeEnabled(enabled: Boolean) {
|
||||
ensureRuntime().setTalkModeEnabled(enabled)
|
||||
}
|
||||
|
||||
fun setSpeakerEnabled(enabled: Boolean) {
|
||||
ensureRuntime().setSpeakerEnabled(enabled)
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@ package ai.openclaw.app
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -21,6 +23,7 @@ class NodeForegroundService : Service() {
|
||||
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private var notificationJob: Job? = null
|
||||
private var didStartForeground = false
|
||||
private var voiceCaptureMode = VoiceCaptureMode.Off
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
@@ -36,22 +39,51 @@ class NodeForegroundService : Service() {
|
||||
notificationJob =
|
||||
scope.launch {
|
||||
combine(
|
||||
runtime.statusText,
|
||||
runtime.serverName,
|
||||
runtime.isConnected,
|
||||
runtime.micEnabled,
|
||||
runtime.micIsListening,
|
||||
) { status, server, connected, micEnabled, micListening ->
|
||||
Quint(status, server, connected, micEnabled, micListening)
|
||||
}.collect { (status, server, connected, micEnabled, micListening) ->
|
||||
val title = if (connected) "OpenClaw Node · Connected" else "OpenClaw Node"
|
||||
val micSuffix =
|
||||
if (micEnabled) {
|
||||
if (micListening) " · Mic: Listening" else " · Mic: Pending"
|
||||
} else {
|
||||
""
|
||||
combine(
|
||||
runtime.statusText,
|
||||
runtime.serverName,
|
||||
runtime.isConnected,
|
||||
runtime.voiceCaptureMode,
|
||||
) { status, server, connected, mode ->
|
||||
VoiceNotificationBase(
|
||||
status = status,
|
||||
server = server,
|
||||
connected = connected,
|
||||
mode = mode,
|
||||
)
|
||||
},
|
||||
combine(
|
||||
runtime.micEnabled,
|
||||
runtime.micIsListening,
|
||||
runtime.talkModeListening,
|
||||
runtime.talkModeSpeaking,
|
||||
) { micEnabled, micListening, talkListening, talkSpeaking ->
|
||||
VoiceNotificationCapture(
|
||||
micEnabled = micEnabled,
|
||||
micListening = micListening,
|
||||
talkListening = talkListening,
|
||||
talkSpeaking = talkSpeaking,
|
||||
)
|
||||
},
|
||||
) { base, capture ->
|
||||
VoiceNotificationState(base = base, capture = capture)
|
||||
}.collect { state ->
|
||||
voiceCaptureMode = state.mode
|
||||
val title =
|
||||
when {
|
||||
state.connected && state.mode == VoiceCaptureMode.TalkMode -> "OpenClaw Node · Talk"
|
||||
state.connected -> "OpenClaw Node · Connected"
|
||||
else -> "OpenClaw Node"
|
||||
}
|
||||
val text = (server?.let { "$status · $it" } ?: status) + micSuffix
|
||||
val text =
|
||||
(state.server?.let { "${state.status} · $it" } ?: state.status) +
|
||||
voiceNotificationSuffix(
|
||||
mode = state.mode,
|
||||
manualMicEnabled = state.capture.micEnabled,
|
||||
manualMicListening = state.capture.micListening,
|
||||
talkListening = state.capture.talkListening,
|
||||
talkSpeaking = state.capture.talkSpeaking,
|
||||
)
|
||||
|
||||
startForegroundWithTypes(
|
||||
notification = buildNotification(title = title, text = text),
|
||||
@@ -60,13 +92,27 @@ class NodeForegroundService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
override fun onStartCommand(
|
||||
intent: Intent?,
|
||||
flags: Int,
|
||||
startId: Int,
|
||||
): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_STOP -> {
|
||||
(application as NodeApp).peekRuntime()?.disconnect()
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
ACTION_SET_VOICE_CAPTURE_MODE -> {
|
||||
voiceCaptureMode = intent.getStringExtra(EXTRA_VOICE_CAPTURE_MODE).toVoiceCaptureMode()
|
||||
startForegroundWithTypes(
|
||||
notification =
|
||||
buildNotification(
|
||||
title = "OpenClaw Node",
|
||||
text = if (voiceCaptureMode == VoiceCaptureMode.TalkMode) "Talk mode active" else "Connected",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
// Keep running; connection is managed by NodeRuntime (auto-reconnect + manual).
|
||||
return START_STICKY
|
||||
@@ -127,17 +173,13 @@ class NodeForegroundService : Service() {
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun updateNotification(notification: Notification) {
|
||||
val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
mgr.notify(NOTIFICATION_ID, notification)
|
||||
}
|
||||
|
||||
private fun startForegroundWithTypes(notification: Notification) {
|
||||
val serviceTypes = foregroundServiceTypesForVoiceMode(voiceCaptureMode)
|
||||
if (didStartForeground) {
|
||||
updateNotification(notification)
|
||||
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
|
||||
return
|
||||
}
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
ServiceCompat.startForeground(this, NOTIFICATION_ID, notification, serviceTypes)
|
||||
didStartForeground = true
|
||||
}
|
||||
|
||||
@@ -146,6 +188,8 @@ class NodeForegroundService : Service() {
|
||||
private const val NOTIFICATION_ID = 1
|
||||
|
||||
private const val ACTION_STOP = "ai.openclaw.app.action.STOP"
|
||||
private const val ACTION_SET_VOICE_CAPTURE_MODE = "ai.openclaw.app.action.SET_VOICE_CAPTURE_MODE"
|
||||
private const val EXTRA_VOICE_CAPTURE_MODE = "ai.openclaw.app.extra.VOICE_CAPTURE_MODE"
|
||||
|
||||
fun start(context: Context) {
|
||||
val intent = Intent(context, NodeForegroundService::class.java)
|
||||
@@ -156,7 +200,85 @@ class NodeForegroundService : Service() {
|
||||
val intent = Intent(context, NodeForegroundService::class.java).setAction(ACTION_STOP)
|
||||
context.startService(intent)
|
||||
}
|
||||
|
||||
fun setVoiceCaptureMode(
|
||||
context: Context,
|
||||
mode: VoiceCaptureMode,
|
||||
) {
|
||||
val intent =
|
||||
Intent(context, NodeForegroundService::class.java)
|
||||
.setAction(ACTION_SET_VOICE_CAPTURE_MODE)
|
||||
.putExtra(EXTRA_VOICE_CAPTURE_MODE, mode.name)
|
||||
if (mode == VoiceCaptureMode.TalkMode) {
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class Quint<A, B, C, D, E>(val first: A, val second: B, val third: C, val fourth: D, val fifth: E)
|
||||
internal fun foregroundServiceTypesForVoiceMode(mode: VoiceCaptureMode): Int {
|
||||
val base = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
||||
return if (mode == VoiceCaptureMode.TalkMode) {
|
||||
base or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
internal fun voiceNotificationSuffix(
|
||||
mode: VoiceCaptureMode,
|
||||
manualMicEnabled: Boolean,
|
||||
manualMicListening: Boolean,
|
||||
talkListening: Boolean,
|
||||
talkSpeaking: Boolean,
|
||||
): String {
|
||||
return when (mode) {
|
||||
VoiceCaptureMode.TalkMode ->
|
||||
when {
|
||||
talkSpeaking -> " · Talk: Speaking"
|
||||
talkListening -> " · Talk: Listening"
|
||||
else -> " · Talk: On"
|
||||
}
|
||||
VoiceCaptureMode.ManualMic ->
|
||||
if (manualMicEnabled) {
|
||||
if (manualMicListening) " · Mic: Listening" else " · Mic: Pending"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
VoiceCaptureMode.Off -> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun String?.toVoiceCaptureMode(): VoiceCaptureMode {
|
||||
return VoiceCaptureMode.entries.firstOrNull { it.name == this } ?: VoiceCaptureMode.Off
|
||||
}
|
||||
|
||||
private data class VoiceNotificationBase(
|
||||
val status: String,
|
||||
val server: String?,
|
||||
val connected: Boolean,
|
||||
val mode: VoiceCaptureMode,
|
||||
)
|
||||
|
||||
private data class VoiceNotificationCapture(
|
||||
val micEnabled: Boolean,
|
||||
val micListening: Boolean,
|
||||
val talkListening: Boolean,
|
||||
val talkSpeaking: Boolean,
|
||||
)
|
||||
|
||||
private data class VoiceNotificationState(
|
||||
val base: VoiceNotificationBase,
|
||||
val capture: VoiceNotificationCapture,
|
||||
) {
|
||||
val status: String
|
||||
get() = base.status
|
||||
val server: String?
|
||||
get() = base.server
|
||||
val connected: Boolean
|
||||
get() = base.connected
|
||||
val mode: VoiceCaptureMode
|
||||
get() = base.mode
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ class NodeRuntime(
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private val externalAudioCaptureActive = MutableStateFlow(false)
|
||||
private val _voiceCaptureMode = MutableStateFlow(VoiceCaptureMode.Off)
|
||||
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = _voiceCaptureMode.asStateFlow()
|
||||
|
||||
private val discovery = GatewayDiscovery(appContext, scope = scope)
|
||||
val gateways: StateFlow<List<GatewayEndpoint>> = discovery.gateways
|
||||
@@ -428,6 +430,18 @@ class NodeRuntime(
|
||||
)
|
||||
}
|
||||
|
||||
val talkModeEnabled: StateFlow<Boolean>
|
||||
get() = talkMode.isEnabled
|
||||
|
||||
val talkModeListening: StateFlow<Boolean>
|
||||
get() = talkMode.isListening
|
||||
|
||||
val talkModeSpeaking: StateFlow<Boolean>
|
||||
get() = talkMode.isSpeaking
|
||||
|
||||
val talkModeStatusText: StateFlow<String>
|
||||
get() = talkMode.statusText
|
||||
|
||||
private fun syncMainSessionKey(agentId: String?) {
|
||||
val resolvedKey = resolveNodeMainSessionKey(agentId)
|
||||
// Always push the resolved session key into TalkMode, even when the
|
||||
@@ -599,17 +613,8 @@ class NodeRuntime(
|
||||
prefs.loadGatewayToken()
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
prefs.talkEnabled.collect { enabled ->
|
||||
// MicCaptureManager handles STT + send to gateway, while the dedicated
|
||||
// reply speaker handles TTS for assistant replies in the voice tab.
|
||||
micCapture.setMicEnabled(enabled)
|
||||
if (enabled) {
|
||||
talkMode.ttsOnAllResponses = false
|
||||
scope.launch { talkMode.ensureChatSubscribed() }
|
||||
}
|
||||
externalAudioCaptureActive.value = enabled
|
||||
}
|
||||
if (prefs.voiceMicEnabled.value) {
|
||||
setVoiceCaptureMode(VoiceCaptureMode.ManualMic, persistManualMic = false)
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.Default) {
|
||||
@@ -643,7 +648,7 @@ class NodeRuntime(
|
||||
if (value) {
|
||||
reconnectPreferredGatewayOnForeground()
|
||||
} else {
|
||||
stopActiveVoiceSession()
|
||||
stopManualVoiceSession()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,21 +762,17 @@ class NodeRuntime(
|
||||
|
||||
fun setVoiceScreenActive(active: Boolean) {
|
||||
if (!active) {
|
||||
stopActiveVoiceSession()
|
||||
stopManualVoiceSession()
|
||||
}
|
||||
// Don't re-enable on active=true; mic toggle drives that
|
||||
}
|
||||
|
||||
fun setMicEnabled(value: Boolean) {
|
||||
prefs.setTalkEnabled(value)
|
||||
if (value) {
|
||||
// Tapping mic on interrupts any active TTS (barge-in)
|
||||
stopVoicePlayback()
|
||||
talkMode.ttsOnAllResponses = false
|
||||
scope.launch { talkMode.ensureChatSubscribed() }
|
||||
}
|
||||
micCapture.setMicEnabled(value)
|
||||
externalAudioCaptureActive.value = value
|
||||
setVoiceCaptureMode(if (value) VoiceCaptureMode.ManualMic else VoiceCaptureMode.Off)
|
||||
}
|
||||
|
||||
fun setTalkModeEnabled(value: Boolean) {
|
||||
setVoiceCaptureMode(if (value) VoiceCaptureMode.TalkMode else VoiceCaptureMode.Off)
|
||||
}
|
||||
|
||||
val speakerEnabled: StateFlow<Boolean>
|
||||
@@ -786,11 +787,72 @@ class NodeRuntime(
|
||||
talkMode.setPlaybackEnabled(value)
|
||||
}
|
||||
|
||||
private fun setVoiceCaptureMode(
|
||||
mode: VoiceCaptureMode,
|
||||
persistManualMic: Boolean = true,
|
||||
) {
|
||||
if (mode == VoiceCaptureMode.TalkMode && !hasRecordAudioPermission()) {
|
||||
_voiceCaptureMode.value = VoiceCaptureMode.Off
|
||||
externalAudioCaptureActive.value = false
|
||||
return
|
||||
}
|
||||
if (_voiceCaptureMode.value == mode) return
|
||||
_voiceCaptureMode.value = mode
|
||||
when (mode) {
|
||||
VoiceCaptureMode.Off -> {
|
||||
talkMode.ttsOnAllResponses = false
|
||||
talkMode.setEnabled(false)
|
||||
stopVoicePlayback()
|
||||
micCapture.setMicEnabled(false)
|
||||
if (persistManualMic) {
|
||||
prefs.setVoiceMicEnabled(false)
|
||||
}
|
||||
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.Off)
|
||||
externalAudioCaptureActive.value = false
|
||||
}
|
||||
|
||||
VoiceCaptureMode.ManualMic -> {
|
||||
talkMode.ttsOnAllResponses = false
|
||||
talkMode.setEnabled(false)
|
||||
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.ManualMic)
|
||||
if (persistManualMic) {
|
||||
prefs.setVoiceMicEnabled(true)
|
||||
}
|
||||
// Tapping mic on interrupts any active TTS (barge-in).
|
||||
stopVoicePlayback()
|
||||
scope.launch { talkMode.ensureChatSubscribed() }
|
||||
micCapture.setMicEnabled(true)
|
||||
externalAudioCaptureActive.value = true
|
||||
}
|
||||
|
||||
VoiceCaptureMode.TalkMode -> {
|
||||
if (persistManualMic) {
|
||||
prefs.setVoiceMicEnabled(false)
|
||||
}
|
||||
micCapture.setMicEnabled(false)
|
||||
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.TalkMode)
|
||||
talkMode.ttsOnAllResponses = true
|
||||
talkMode.setPlaybackEnabled(speakerEnabled.value)
|
||||
scope.launch { talkMode.ensureChatSubscribed() }
|
||||
talkMode.setEnabled(true)
|
||||
externalAudioCaptureActive.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopManualVoiceSession() {
|
||||
if (_voiceCaptureMode.value != VoiceCaptureMode.ManualMic) return
|
||||
setVoiceCaptureMode(VoiceCaptureMode.Off)
|
||||
}
|
||||
|
||||
private fun stopActiveVoiceSession() {
|
||||
talkMode.ttsOnAllResponses = false
|
||||
talkMode.setEnabled(false)
|
||||
stopVoicePlayback()
|
||||
micCapture.setMicEnabled(false)
|
||||
prefs.setTalkEnabled(false)
|
||||
prefs.setVoiceMicEnabled(false)
|
||||
NodeForegroundService.setVoiceCaptureMode(appContext, VoiceCaptureMode.Off)
|
||||
_voiceCaptureMode.value = VoiceCaptureMode.Off
|
||||
externalAudioCaptureActive.value = false
|
||||
}
|
||||
|
||||
@@ -970,6 +1032,7 @@ class NodeRuntime(
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
stopActiveVoiceSession()
|
||||
connectedEndpoint = null
|
||||
activeGatewayAuth = null
|
||||
_pendingGatewayTrust.value = null
|
||||
|
||||
@@ -37,6 +37,7 @@ class SecurePrefs(
|
||||
private const val notificationsForwardingMaxEventsPerMinuteKey =
|
||||
"notifications.forwarding.maxEventsPerMinute"
|
||||
private const val notificationsForwardingSessionKeyKey = "notifications.forwarding.sessionKey"
|
||||
private const val voiceMicEnabledKey = "voice.micEnabled"
|
||||
}
|
||||
|
||||
private val appContext = context.applicationContext
|
||||
@@ -162,8 +163,8 @@ class SecurePrefs(
|
||||
private val _voiceWakeMode = MutableStateFlow(loadVoiceWakeMode())
|
||||
val voiceWakeMode: StateFlow<VoiceWakeMode> = _voiceWakeMode
|
||||
|
||||
private val _talkEnabled = MutableStateFlow(plainPrefs.getBoolean("talk.enabled", false))
|
||||
val talkEnabled: StateFlow<Boolean> = _talkEnabled
|
||||
private val _voiceMicEnabled = MutableStateFlow(plainPrefs.getBoolean(voiceMicEnabledKey, false))
|
||||
val voiceMicEnabled: StateFlow<Boolean> = _voiceMicEnabled
|
||||
|
||||
private val _speakerEnabled = MutableStateFlow(plainPrefs.getBoolean("voice.speakerEnabled", true))
|
||||
val speakerEnabled: StateFlow<Boolean> = _speakerEnabled
|
||||
@@ -478,9 +479,9 @@ class SecurePrefs(
|
||||
_voiceWakeMode.value = mode
|
||||
}
|
||||
|
||||
fun setTalkEnabled(value: Boolean) {
|
||||
plainPrefs.edit { putBoolean("talk.enabled", value) }
|
||||
_talkEnabled.value = value
|
||||
fun setVoiceMicEnabled(value: Boolean) {
|
||||
plainPrefs.edit { putBoolean(voiceMicEnabledKey, value) }
|
||||
_voiceMicEnabled.value = value
|
||||
}
|
||||
|
||||
fun setSpeakerEnabled(value: Boolean) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package ai.openclaw.app
|
||||
|
||||
enum class VoiceCaptureMode {
|
||||
Off,
|
||||
ManualMic,
|
||||
TalkMode,
|
||||
}
|
||||
@@ -35,10 +35,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.filled.MicOff
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeOff
|
||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||
import androidx.compose.material.icons.filled.Mic
|
||||
import androidx.compose.material.icons.filled.MicOff
|
||||
import androidx.compose.material.icons.filled.RecordVoiceOver
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -69,6 +70,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import ai.openclaw.app.MainViewModel
|
||||
import ai.openclaw.app.VoiceCaptureMode
|
||||
import ai.openclaw.app.voice.VoiceConversationEntry
|
||||
import ai.openclaw.app.voice.VoiceConversationRole
|
||||
import kotlin.math.max
|
||||
@@ -81,6 +83,7 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val gatewayStatus by viewModel.statusText.collectAsState()
|
||||
val voiceCaptureMode by viewModel.voiceCaptureMode.collectAsState()
|
||||
val micEnabled by viewModel.micEnabled.collectAsState()
|
||||
val micCooldown by viewModel.micCooldown.collectAsState()
|
||||
val speakerEnabled by viewModel.speakerEnabled.collectAsState()
|
||||
@@ -90,12 +93,15 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
val micConversation by viewModel.micConversation.collectAsState()
|
||||
val micInputLevel by viewModel.micInputLevel.collectAsState()
|
||||
val micIsSending by viewModel.micIsSending.collectAsState()
|
||||
val talkModeEnabled by viewModel.talkModeEnabled.collectAsState()
|
||||
val talkModeListening by viewModel.talkModeListening.collectAsState()
|
||||
val talkModeSpeaking by viewModel.talkModeSpeaking.collectAsState()
|
||||
|
||||
val hasStreamingAssistant = micConversation.any { it.role == VoiceConversationRole.Assistant && it.isStreaming }
|
||||
val showThinkingBubble = micIsSending && !hasStreamingAssistant
|
||||
|
||||
var hasMicPermission by remember { mutableStateOf(context.hasRecordAudioPermission()) }
|
||||
var pendingMicEnable by remember { mutableStateOf(false) }
|
||||
var pendingVoicePermissionAction by remember { mutableStateOf<PendingVoicePermissionAction?>(null) }
|
||||
|
||||
DisposableEffect(lifecycleOwner, context) {
|
||||
val observer =
|
||||
@@ -107,7 +113,7 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
// Stop TTS when leaving the voice screen
|
||||
// Manual mic is tied to the Voice tab; Talk Mode is explicit and can continue.
|
||||
viewModel.setVoiceScreenActive(false)
|
||||
}
|
||||
}
|
||||
@@ -115,10 +121,14 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
val requestMicPermission =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
hasMicPermission = granted
|
||||
if (granted && pendingMicEnable) {
|
||||
viewModel.setMicEnabled(true)
|
||||
if (granted) {
|
||||
when (pendingVoicePermissionAction) {
|
||||
PendingVoicePermissionAction.ManualMic -> viewModel.setMicEnabled(true)
|
||||
PendingVoicePermissionAction.TalkMode -> viewModel.setTalkModeEnabled(true)
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
pendingMicEnable = false
|
||||
pendingVoicePermissionAction = null
|
||||
}
|
||||
|
||||
LaunchedEffect(micConversation.size, showThinkingBubble) {
|
||||
@@ -161,12 +171,12 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
tint = mobileTextTertiary,
|
||||
)
|
||||
Text(
|
||||
"Tap the mic to start",
|
||||
"Tap mic or Talk",
|
||||
style = mobileHeadline,
|
||||
color = mobileTextSecondary,
|
||||
)
|
||||
Text(
|
||||
"Each pause sends a turn automatically.",
|
||||
"Mic sends turns; Talk keeps the conversation open.",
|
||||
style = mobileCallout,
|
||||
color = mobileTextTertiary,
|
||||
)
|
||||
@@ -263,7 +273,7 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
if (hasMicPermission) {
|
||||
viewModel.setMicEnabled(true)
|
||||
} else {
|
||||
pendingMicEnable = true
|
||||
pendingVoicePermissionAction = PendingVoicePermissionAction.ManualMic
|
||||
requestMicPermission.launch(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
},
|
||||
@@ -287,11 +297,39 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
// Invisible spacer to balance the row (matches speaker column width)
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(modifier = Modifier.size(48.dp))
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (talkModeEnabled) {
|
||||
viewModel.setTalkModeEnabled(false)
|
||||
return@IconButton
|
||||
}
|
||||
if (hasMicPermission) {
|
||||
viewModel.setTalkModeEnabled(true)
|
||||
} else {
|
||||
pendingVoicePermissionAction = PendingVoicePermissionAction.TalkMode
|
||||
requestMicPermission.launch(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(48.dp),
|
||||
colors =
|
||||
IconButtonDefaults.iconButtonColors(
|
||||
containerColor = if (talkModeEnabled) mobileSuccessSoft else mobileSurface,
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RecordVoiceOver,
|
||||
contentDescription = if (talkModeEnabled) "Turn Talk Mode off" else "Turn Talk Mode on",
|
||||
modifier = Modifier.size(22.dp),
|
||||
tint = if (talkModeEnabled) mobileSuccess else mobileTextSecondary,
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text("", style = mobileCaption2)
|
||||
Text(
|
||||
if (talkModeEnabled) "Talk on" else "Talk",
|
||||
style = mobileCaption2,
|
||||
color = if (talkModeEnabled) mobileSuccess else mobileTextTertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,6 +337,9 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
val queueCount = micQueuedMessages.size
|
||||
val stateText =
|
||||
when {
|
||||
voiceCaptureMode == VoiceCaptureMode.TalkMode && talkModeSpeaking -> "Talk speaking"
|
||||
voiceCaptureMode == VoiceCaptureMode.TalkMode && talkModeListening -> "Talk listening"
|
||||
voiceCaptureMode == VoiceCaptureMode.TalkMode -> "Talk on"
|
||||
queueCount > 0 -> "$queueCount queued"
|
||||
micIsSending -> "Sending"
|
||||
micCooldown -> "Cooldown"
|
||||
@@ -307,14 +348,15 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
}
|
||||
val stateColor =
|
||||
when {
|
||||
voiceCaptureMode == VoiceCaptureMode.TalkMode -> mobileSuccess
|
||||
micEnabled -> mobileSuccess
|
||||
micIsSending -> mobileAccent
|
||||
else -> mobileTextSecondary
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = if (micEnabled) mobileSuccessSoft else mobileSurface,
|
||||
border = BorderStroke(1.dp, if (micEnabled) mobileSuccess.copy(alpha = 0.3f) else mobileBorder),
|
||||
color = if (micEnabled || talkModeEnabled) mobileSuccessSoft else mobileSurface,
|
||||
border = BorderStroke(1.dp, if (micEnabled || talkModeEnabled) mobileSuccess.copy(alpha = 0.3f) else mobileBorder),
|
||||
) {
|
||||
Text(
|
||||
"$gatewayStatus · $stateText",
|
||||
@@ -353,6 +395,11 @@ fun VoiceTabScreen(viewModel: MainViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
private enum class PendingVoicePermissionAction {
|
||||
ManualMic,
|
||||
TalkMode,
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VoiceTurnBubble(entry: VoiceConversationEntry) {
|
||||
val isUser = entry.role == VoiceConversationRole.User
|
||||
|
||||
@@ -226,14 +226,15 @@ class TalkModeManager(
|
||||
// If this is a response we initiated, handle normally below.
|
||||
// Otherwise, if ttsOnAllResponses, finish streaming TTS on terminal events.
|
||||
val pending = pendingRunId
|
||||
if (pending == null || runId != pending) {
|
||||
val knownRun = pending == runId || hasRunCompletion(runId)
|
||||
if (!knownRun) {
|
||||
if (ttsOnAllResponses && state == "final") {
|
||||
val text = extractTextFromChatEventMessage(obj["message"])
|
||||
if (!text.isNullOrBlank()) {
|
||||
playTtsForText(text)
|
||||
}
|
||||
}
|
||||
if (pending == null || runId != pending) return
|
||||
return
|
||||
}
|
||||
Log.d(tag, "chat event arrived runId=$runId state=$state pendingRunId=$pendingRunId")
|
||||
val terminal =
|
||||
@@ -539,6 +540,7 @@ class TalkModeManager(
|
||||
|
||||
private suspend fun sendChat(message: String, session: GatewaySession): String {
|
||||
val runId = UUID.randomUUID().toString()
|
||||
armPendingRun(runId)
|
||||
val params =
|
||||
buildJsonObject {
|
||||
put("sessionKey", JsonPrimitive(mainSessionKey.ifBlank { "main" }))
|
||||
@@ -547,19 +549,29 @@ class TalkModeManager(
|
||||
put("timeoutMs", JsonPrimitive(30_000))
|
||||
put("idempotencyKey", JsonPrimitive(runId))
|
||||
}
|
||||
val res = session.request("chat.send", params.toString())
|
||||
val parsed = parseRunId(res) ?: runId
|
||||
if (parsed != runId) {
|
||||
pendingRunId = parsed
|
||||
try {
|
||||
val res = session.request("chat.send", params.toString())
|
||||
val parsed = parseRunId(res) ?: runId
|
||||
if (parsed != runId) {
|
||||
pendingRunId = parsed
|
||||
}
|
||||
return parsed
|
||||
} catch (err: Throwable) {
|
||||
clearPendingRun(runId)
|
||||
throw err
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
private suspend fun waitForChatFinal(runId: String): Boolean {
|
||||
pendingFinal?.cancel()
|
||||
val deferred = CompletableDeferred<Boolean>()
|
||||
pendingRunId = runId
|
||||
pendingFinal = deferred
|
||||
consumeRunCompletion(runId)?.let { return it }
|
||||
val deferred =
|
||||
if (pendingRunId == runId) {
|
||||
pendingFinal ?: armPendingRun(runId)
|
||||
} else {
|
||||
armPendingRun(runId)
|
||||
}
|
||||
|
||||
consumeRunCompletion(runId)?.let { return it }
|
||||
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -570,11 +582,25 @@ class TalkModeManager(
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
if (!result && pendingRunId == runId) {
|
||||
clearPendingRun(runId)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun armPendingRun(runId: String): CompletableDeferred<Boolean> {
|
||||
pendingFinal?.cancel()
|
||||
val deferred = CompletableDeferred<Boolean>()
|
||||
pendingRunId = runId
|
||||
pendingFinal = deferred
|
||||
return deferred
|
||||
}
|
||||
|
||||
private fun clearPendingRun(runId: String) {
|
||||
if (pendingRunId == runId) {
|
||||
pendingFinal = null
|
||||
pendingRunId = null
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun cacheRunCompletion(runId: String, isFinal: Boolean) {
|
||||
@@ -593,6 +619,12 @@ class TalkModeManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasRunCompletion(runId: String): Boolean {
|
||||
synchronized(completedRunsLock) {
|
||||
return completedRunStates.containsKey(runId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun consumeRunText(runId: String): String? {
|
||||
synchronized(completedRunsLock) {
|
||||
return completedRunTexts.remove(runId)
|
||||
|
||||
@@ -2,6 +2,7 @@ package ai.openclaw.app
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Test
|
||||
@@ -30,6 +31,35 @@ class NodeForegroundServiceTest {
|
||||
assertEquals(expectedFlags, savedIntent.flags and expectedFlags)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun foregroundServiceTypesForVoiceMode_addsMicrophoneOnlyForTalkMode() {
|
||||
assertEquals(
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.Off),
|
||||
)
|
||||
assertEquals(
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.ManualMic),
|
||||
)
|
||||
assertEquals(
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,
|
||||
foregroundServiceTypesForVoiceMode(VoiceCaptureMode.TalkMode),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun voiceNotificationSuffixReflectsActiveCaptureMode() {
|
||||
assertEquals("", voiceNotificationSuffix(VoiceCaptureMode.Off, false, false, false, false))
|
||||
assertEquals(
|
||||
" · Mic: Listening",
|
||||
voiceNotificationSuffix(VoiceCaptureMode.ManualMic, true, true, false, false),
|
||||
)
|
||||
assertEquals(
|
||||
" · Talk: Speaking",
|
||||
voiceNotificationSuffix(VoiceCaptureMode.TalkMode, false, false, true, true),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildNotification(service: NodeForegroundService): Notification {
|
||||
val method =
|
||||
NodeForegroundService::class.java.getDeclaredMethod(
|
||||
|
||||
@@ -2,7 +2,9 @@ package ai.openclaw.app
|
||||
|
||||
import android.content.Context
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
@@ -22,6 +24,32 @@ class SecurePrefsTest {
|
||||
assertEquals("whileUsing", plainPrefs.getString("location.enabledMode", null))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun voiceMicEnabled_ignoresOldTalkEnabledKey() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
|
||||
plainPrefs.edit().clear().putBoolean("talk.enabled", true).commit()
|
||||
|
||||
val prefs = SecurePrefs(context)
|
||||
|
||||
assertFalse(prefs.voiceMicEnabled.value)
|
||||
assertFalse(plainPrefs.contains("voice.micEnabled"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun setVoiceMicEnabled_persistsNewKeyOnly() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
|
||||
plainPrefs.edit().clear().putBoolean("talk.enabled", false).commit()
|
||||
val prefs = SecurePrefs(context)
|
||||
|
||||
prefs.setVoiceMicEnabled(true)
|
||||
|
||||
assertTrue(prefs.voiceMicEnabled.value)
|
||||
assertTrue(plainPrefs.getBoolean("voice.micEnabled", false))
|
||||
assertFalse(plainPrefs.getBoolean("talk.enabled", false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun saveGatewayBootstrapToken_persistsSeparatelyFromSharedToken() {
|
||||
val context = RuntimeEnvironment.getApplication()
|
||||
|
||||
@@ -5,6 +5,7 @@ import ai.openclaw.app.gateway.DeviceAuthTokenStore
|
||||
import ai.openclaw.app.gateway.DeviceIdentityStore
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -49,6 +50,34 @@ class TalkModeManagerTest {
|
||||
assertEquals(12L, playbackGeneration(manager).get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun duplicateFinalForPendingTalkRunDoesNotStartAllResponseTts() {
|
||||
val manager = createManager()
|
||||
val final = CompletableDeferred<Boolean>()
|
||||
|
||||
manager.ttsOnAllResponses = true
|
||||
setPrivateField(manager, "pendingRunId", "run-talk")
|
||||
setPrivateField(manager, "pendingFinal", final)
|
||||
|
||||
manager.handleGatewayEvent("chat", chatFinalPayload(runId = "run-talk", text = "spoken once"))
|
||||
assertTrue(final.isCompleted)
|
||||
assertEquals(0L, playbackGeneration(manager).get())
|
||||
|
||||
manager.handleGatewayEvent("chat", chatFinalPayload(runId = "run-talk", text = "spoken once"))
|
||||
|
||||
assertEquals(0L, playbackGeneration(manager).get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nonPendingFinalStillUsesAllResponseTts() {
|
||||
val manager = createManager()
|
||||
|
||||
manager.ttsOnAllResponses = true
|
||||
manager.handleGatewayEvent("chat", chatFinalPayload(runId = "run-other", text = "speak this"))
|
||||
|
||||
assertEquals(1L, playbackGeneration(manager).get())
|
||||
}
|
||||
|
||||
private fun createManager(): TalkModeManager {
|
||||
val app = RuntimeEnvironment.getApplication()
|
||||
val sessionJob = SupervisorJob()
|
||||
@@ -86,6 +115,22 @@ class TalkModeManagerTest {
|
||||
field.isAccessible = true
|
||||
return field.get(target)
|
||||
}
|
||||
|
||||
private fun chatFinalPayload(runId: String, text: String): String {
|
||||
return """
|
||||
{
|
||||
"runId": "$runId",
|
||||
"sessionKey": "main",
|
||||
"state": "final",
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{ "type": "text", "text": "$text" }
|
||||
]
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
private class InMemoryDeviceAuthStore : DeviceAuthTokenStore {
|
||||
|
||||
@@ -21,6 +21,7 @@ struct SettingsTab: View {
|
||||
@AppStorage("node.instanceId") private var instanceId: String = UUID().uuidString
|
||||
@AppStorage("voiceWake.enabled") private var voiceWakeEnabled: Bool = false
|
||||
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
|
||||
@AppStorage(TalkSpeechLocale.storageKey) private var talkSpeechLocale: String = TalkSpeechLocale.automaticID
|
||||
@AppStorage("talk.button.enabled") private var talkButtonEnabled: Bool = true
|
||||
@AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
|
||||
@AppStorage("camera.enabled") private var cameraEnabled: Bool = true
|
||||
@@ -278,6 +279,11 @@ struct SettingsTab: View {
|
||||
help: "Enables voice conversation mode with your connected OpenClaw agent.") { newValue in
|
||||
self.appModel.setTalkEnabled(newValue)
|
||||
}
|
||||
Picker("Speech Language", selection: self.$talkSpeechLocale) {
|
||||
ForEach(TalkSpeechLocale.supportedOptions()) { option in
|
||||
Text(option.label).tag(option.id)
|
||||
}
|
||||
}
|
||||
self.featureToggle(
|
||||
"Background Listening",
|
||||
isOn: self.$talkBackgroundEnabled,
|
||||
|
||||
@@ -12,6 +12,7 @@ struct TalkModeGatewayConfigState {
|
||||
let rawConfigApiKey: String?
|
||||
let interruptOnSpeech: Bool?
|
||||
let silenceTimeoutMs: Int
|
||||
let speechLocaleID: String?
|
||||
}
|
||||
|
||||
enum TalkModeGatewayConfigParser {
|
||||
@@ -53,6 +54,7 @@ enum TalkModeGatewayConfigParser {
|
||||
let silenceTimeoutMs = TalkConfigParsing.resolvedSilenceTimeoutMs(
|
||||
talk,
|
||||
fallback: defaultSilenceTimeoutMs)
|
||||
let speechLocaleID = TalkConfigParsing.resolvedSpeechLocaleID(talk)
|
||||
|
||||
return TalkModeGatewayConfigState(
|
||||
activeProvider: activeProvider,
|
||||
@@ -64,6 +66,7 @@ enum TalkModeGatewayConfigParser {
|
||||
defaultOutputFormat: defaultOutputFormat,
|
||||
rawConfigApiKey: rawConfigApiKey,
|
||||
interruptOnSpeech: interruptOnSpeech,
|
||||
silenceTimeoutMs: silenceTimeoutMs)
|
||||
silenceTimeoutMs: silenceTimeoutMs,
|
||||
speechLocaleID: speechLocaleID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@ final class TalkModeManager: NSObject {
|
||||
private var apiKey: String?
|
||||
private var voiceAliases: [String: String] = [:]
|
||||
private var interruptOnSpeech: Bool = true
|
||||
private var gatewaySpeechLocaleID: String?
|
||||
private var mainSessionKey: String = "main"
|
||||
private var fallbackVoiceId: String?
|
||||
private var lastPlaybackWasPCM: Bool = false
|
||||
@@ -500,12 +501,17 @@ final class TalkModeManager: NSObject {
|
||||
#endif
|
||||
|
||||
self.stopRecognition()
|
||||
self.speechRecognizer = SFSpeechRecognizer()
|
||||
let localSpeechLocale = UserDefaults.standard.string(forKey: TalkSpeechLocale.storageKey)
|
||||
let resolvedSpeech = TalkSpeechLocale.makeRecognizer(
|
||||
localSelection: localSpeechLocale,
|
||||
gatewaySelection: self.gatewaySpeechLocaleID)
|
||||
self.speechRecognizer = resolvedSpeech.recognizer
|
||||
guard let recognizer = self.speechRecognizer else {
|
||||
throw NSError(domain: "TalkMode", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Speech recognizer unavailable",
|
||||
])
|
||||
}
|
||||
GatewayDiagnostics.log("talk speech: locale=\(resolvedSpeech.localeID ?? "default")")
|
||||
|
||||
self.recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
|
||||
self.recognitionRequest?.shouldReportPartialResults = true
|
||||
@@ -2027,6 +2033,7 @@ extension TalkModeManager {
|
||||
if let interrupt = parsed.interruptOnSpeech {
|
||||
self.interruptOnSpeech = interrupt
|
||||
}
|
||||
self.gatewaySpeechLocaleID = parsed.speechLocaleID
|
||||
self.silenceWindow = TimeInterval(parsed.silenceTimeoutMs) / 1000
|
||||
if parsed.normalizedPayload || parsed.defaultVoiceId != nil || parsed.rawConfigApiKey != nil {
|
||||
GatewayDiagnostics.log(
|
||||
@@ -2041,6 +2048,7 @@ extension TalkModeManager {
|
||||
self.gatewayTalkDefaultModelId = nil
|
||||
self.gatewayTalkApiKeyConfigured = false
|
||||
self.gatewayTalkConfigLoaded = false
|
||||
self.gatewaySpeechLocaleID = nil
|
||||
self.silenceWindow = TimeInterval(Self.defaultSilenceTimeoutMs) / 1000
|
||||
}
|
||||
}
|
||||
|
||||
100
apps/ios/Sources/Voice/TalkSpeechLocale.swift
Normal file
100
apps/ios/Sources/Voice/TalkSpeechLocale.swift
Normal file
@@ -0,0 +1,100 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import Speech
|
||||
|
||||
enum TalkSpeechLocale {
|
||||
static let storageKey = "talk.speechLocale"
|
||||
static let automaticID = "auto"
|
||||
static let fallbackLocaleID = "en-US"
|
||||
|
||||
struct Option: Identifiable {
|
||||
let id: String
|
||||
let label: String
|
||||
}
|
||||
|
||||
static func supportedOptions(
|
||||
supportedLocales: Set<Locale> = SFSpeechRecognizer.supportedLocales()
|
||||
) -> [Option] {
|
||||
var seen = Set<String>()
|
||||
let dynamic: [Option] = supportedLocales
|
||||
.compactMap { locale in
|
||||
let id = self.canonicalID(locale.identifier)
|
||||
guard seen.insert(id).inserted else { return nil }
|
||||
return Option(id: id, label: self.friendlyName(for: locale))
|
||||
}
|
||||
.sorted { (lhs: Option, rhs: Option) in
|
||||
lhs.label.localizedCaseInsensitiveCompare(rhs.label) == .orderedAscending
|
||||
}
|
||||
return [Option(id: self.automaticID, label: "Automatic")] + dynamic
|
||||
}
|
||||
|
||||
static func resolvedLocaleID(
|
||||
localSelection: String?,
|
||||
gatewaySelection: String?,
|
||||
deviceLocaleID: String = Locale.autoupdatingCurrent.identifier,
|
||||
fallbackLocaleID: String = Self.fallbackLocaleID,
|
||||
supportedLocaleIDs: Set<String>
|
||||
) -> String? {
|
||||
TalkConfigParsing.resolvedSpeechRecognitionLocaleID(
|
||||
preferredLocaleIDs: [
|
||||
TalkConfigParsing.normalizedExplicitSpeechLocaleID(localSelection),
|
||||
TalkConfigParsing.normalizedExplicitSpeechLocaleID(gatewaySelection),
|
||||
deviceLocaleID,
|
||||
],
|
||||
fallbackLocaleID: fallbackLocaleID,
|
||||
supportedLocaleIDs: supportedLocaleIDs)
|
||||
}
|
||||
|
||||
static func makeRecognizer(
|
||||
localSelection: String?,
|
||||
gatewaySelection: String?,
|
||||
supportedLocales: Set<Locale> = SFSpeechRecognizer.supportedLocales()
|
||||
) -> (recognizer: SFSpeechRecognizer?, localeID: String?) {
|
||||
let supportedIDs = Set(supportedLocales.map(\.identifier))
|
||||
guard let localeID = self.resolvedLocaleID(
|
||||
localSelection: localSelection,
|
||||
gatewaySelection: gatewaySelection,
|
||||
supportedLocaleIDs: supportedIDs)
|
||||
else {
|
||||
let recognizer = SFSpeechRecognizer()
|
||||
return (recognizer, recognizer?.locale.identifier)
|
||||
}
|
||||
|
||||
if let recognizer = SFSpeechRecognizer(locale: Locale(identifier: localeID)) {
|
||||
return (recognizer, localeID)
|
||||
}
|
||||
|
||||
let recognizer = SFSpeechRecognizer()
|
||||
return (recognizer, recognizer?.locale.identifier)
|
||||
}
|
||||
|
||||
static func normalizedExplicitLocaleID(_ raw: String?) -> String? {
|
||||
TalkConfigParsing.normalizedExplicitSpeechLocaleID(raw, automaticID: self.automaticID)
|
||||
}
|
||||
|
||||
private static func normalizedLocaleID(_ raw: String?) -> String? {
|
||||
TalkConfigParsing.normalizedSpeechLocaleID(raw)
|
||||
}
|
||||
|
||||
private static func canonicalID(_ raw: String) -> String {
|
||||
raw.replacingOccurrences(of: "_", with: "-")
|
||||
}
|
||||
|
||||
private static func friendlyName(for locale: Locale) -> String {
|
||||
let id = self.canonicalID(locale.identifier)
|
||||
let cleanLocale = Locale(identifier: id)
|
||||
if let langCode = cleanLocale.language.languageCode?.identifier,
|
||||
let lang = cleanLocale.localizedString(forLanguageCode: langCode),
|
||||
let regionCode = cleanLocale.region?.identifier,
|
||||
let region = cleanLocale.localizedString(forRegionCode: regionCode)
|
||||
{
|
||||
return "\(lang) (\(region))"
|
||||
}
|
||||
if let langCode = cleanLocale.language.languageCode?.identifier,
|
||||
let lang = cleanLocale.localizedString(forLanguageCode: langCode)
|
||||
{
|
||||
return lang
|
||||
}
|
||||
return cleanLocale.localizedString(forIdentifier: id) ?? id
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,16 @@ private let iOSSilenceTimeoutMs = 900
|
||||
fallback: iOSSilenceTimeoutMs) == 1500)
|
||||
}
|
||||
|
||||
@Test func readsConfiguredSpeechLocale() {
|
||||
let talk: [String: Any] = [
|
||||
"speechLocale": " ru-RU ",
|
||||
]
|
||||
|
||||
#expect(
|
||||
TalkConfigParsing.resolvedSpeechLocaleID(
|
||||
TalkConfigParsing.bridgeFoundationDictionary(talk)) == "ru-RU")
|
||||
}
|
||||
|
||||
@Test func defaultsSilenceTimeoutMsWhenMissing() {
|
||||
#expect(TalkConfigParsing.resolvedSilenceTimeoutMs(nil, fallback: iOSSilenceTimeoutMs) == iOSSilenceTimeoutMs)
|
||||
}
|
||||
|
||||
41
apps/ios/Tests/TalkSpeechLocaleTests.swift
Normal file
41
apps/ios/Tests/TalkSpeechLocaleTests.swift
Normal file
@@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@Suite struct TalkSpeechLocaleTests {
|
||||
@Test func localSelectionOverridesGatewayConfig() {
|
||||
let locale = TalkSpeechLocale.resolvedLocaleID(
|
||||
localSelection: "de-DE",
|
||||
gatewaySelection: "ru-RU",
|
||||
deviceLocaleID: "en-US",
|
||||
supportedLocaleIDs: ["de-DE", "ru-RU", "en-US"])
|
||||
|
||||
#expect(locale == "de-DE")
|
||||
}
|
||||
|
||||
@Test func automaticLocalSelectionAllowsGatewayConfig() {
|
||||
let locale = TalkSpeechLocale.resolvedLocaleID(
|
||||
localSelection: TalkSpeechLocale.automaticID,
|
||||
gatewaySelection: "ru_RU",
|
||||
deviceLocaleID: "en-US",
|
||||
supportedLocaleIDs: ["ru-RU", "en-US"])
|
||||
|
||||
#expect(locale == "ru-RU")
|
||||
}
|
||||
|
||||
@Test func unsupportedConfiguredLocaleFallsBackToDeviceThenEnglish() {
|
||||
let deviceLocale = TalkSpeechLocale.resolvedLocaleID(
|
||||
localSelection: "zz-ZZ",
|
||||
gatewaySelection: nil,
|
||||
deviceLocaleID: "fr-FR",
|
||||
supportedLocaleIDs: ["fr-FR", "en-US"])
|
||||
let english = TalkSpeechLocale.resolvedLocaleID(
|
||||
localSelection: "zz-ZZ",
|
||||
gatewaySelection: nil,
|
||||
deviceLocaleID: "yy-YY",
|
||||
supportedLocaleIDs: ["en-US"])
|
||||
|
||||
#expect(deviceLocale == "fr-FR")
|
||||
#expect(english == "en-US")
|
||||
}
|
||||
}
|
||||
@@ -176,6 +176,23 @@ final class AppState {
|
||||
}
|
||||
}
|
||||
|
||||
var talkPhaseSoundsEnabled: Bool {
|
||||
didSet {
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.talkPhaseSoundsEnabled, forKey: talkPhaseSoundsEnabledKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var talkShiftToStopEnabled: Bool {
|
||||
didSet {
|
||||
self.ifNotPreview {
|
||||
UserDefaults.standard.set(self.talkShiftToStopEnabled, forKey: talkShiftToStopEnabledKey)
|
||||
Task { TalkSpeechInterruptMonitor.shared.setEnabled(self.talkShiftToStopEnabled && self.talkEnabled) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gateway-provided UI accent color (hex). Optional; clients provide a default.
|
||||
var seamColorHex: String?
|
||||
|
||||
@@ -309,6 +326,18 @@ final class AppState {
|
||||
self.voiceWakeTriggersTalkMode = UserDefaults.standard
|
||||
.object(forKey: voiceWakeTriggersTalkModeKey) as? Bool ?? false
|
||||
self.talkEnabled = UserDefaults.standard.bool(forKey: talkEnabledKey)
|
||||
if let storedPhaseSounds = UserDefaults.standard.object(forKey: talkPhaseSoundsEnabledKey) as? Bool {
|
||||
self.talkPhaseSoundsEnabled = storedPhaseSounds
|
||||
} else {
|
||||
self.talkPhaseSoundsEnabled = true
|
||||
UserDefaults.standard.set(true, forKey: talkPhaseSoundsEnabledKey)
|
||||
}
|
||||
if let storedShiftToStop = UserDefaults.standard.object(forKey: talkShiftToStopEnabledKey) as? Bool {
|
||||
self.talkShiftToStopEnabled = storedShiftToStop
|
||||
} else {
|
||||
self.talkShiftToStopEnabled = true
|
||||
UserDefaults.standard.set(true, forKey: talkShiftToStopEnabledKey)
|
||||
}
|
||||
self.seamColorHex = nil
|
||||
if let storedHeartbeats = UserDefaults.standard.object(forKey: heartbeatsEnabledKey) as? Bool {
|
||||
self.heartbeatsEnabled = storedHeartbeats
|
||||
@@ -778,6 +807,8 @@ extension AppState {
|
||||
state.voiceWakeAdditionalLocaleIDs = ["en-US", "de-DE"]
|
||||
state.voicePushToTalkEnabled = false
|
||||
state.talkEnabled = false
|
||||
state.talkPhaseSoundsEnabled = true
|
||||
state.talkShiftToStopEnabled = true
|
||||
state.iconOverride = .system
|
||||
state.heartbeatsEnabled = true
|
||||
state.connectionMode = .local
|
||||
|
||||
@@ -24,6 +24,8 @@ let voiceWakeAdditionalLocalesKey = "openclaw.voiceWakeAdditionalLocaleIDs"
|
||||
let voicePushToTalkEnabledKey = "openclaw.voicePushToTalkEnabled"
|
||||
let voiceWakeTriggersTalkModeKey = "openclaw.voiceWakeTriggersTalkMode"
|
||||
let talkEnabledKey = "openclaw.talkEnabled"
|
||||
let talkPhaseSoundsEnabledKey = "openclaw.talkPhaseSoundsEnabled"
|
||||
let talkShiftToStopEnabledKey = "openclaw.talkShiftToStopEnabled"
|
||||
let iconOverrideKey = "openclaw.iconOverride"
|
||||
let connectionModeKey = "openclaw.connectionMode"
|
||||
let remoteTargetKey = "openclaw.remoteTarget"
|
||||
|
||||
@@ -14,7 +14,8 @@ enum ExecAllowlistMatcher {
|
||||
if self.matches(pattern: pattern, target: target) { return entry }
|
||||
} else if pattern != "*",
|
||||
!ExecApprovalHelpers.patternHasPathSelector(rawExecutable),
|
||||
self.matchesExecutableBasename(pattern: pattern, resolution: resolution) {
|
||||
self.matchesExecutableBasename(pattern: pattern, resolution: resolution)
|
||||
{
|
||||
return entry
|
||||
}
|
||||
case .invalid:
|
||||
|
||||
@@ -618,7 +618,8 @@ enum ExecApprovalsStore {
|
||||
|
||||
if !ExecApprovalHelpers.patternHasPathSelector(trimmedPattern),
|
||||
!trimmedResolved.isEmpty,
|
||||
case let .valid(migratedPattern) = ExecApprovalHelpers.validateAllowlistPattern(trimmedResolved) {
|
||||
case let .valid(migratedPattern) = ExecApprovalHelpers.validateAllowlistPattern(trimmedResolved)
|
||||
{
|
||||
return ExecAllowlistEntry(
|
||||
id: entry.id,
|
||||
pattern: migratedPattern,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import AppKit
|
||||
import Observation
|
||||
|
||||
@MainActor
|
||||
@@ -17,6 +18,10 @@ final class TalkModeController {
|
||||
} else {
|
||||
TalkOverlayController.shared.dismiss()
|
||||
}
|
||||
TalkSpeechInterruptMonitor.shared.setEnabled(enabled && AppStateStore.shared.talkShiftToStopEnabled)
|
||||
// Talk Mode and Push-to-Talk share the right Option key — disable PTT while Talk Mode is active.
|
||||
let pttEnabled = !enabled && AppStateStore.shared.voicePushToTalkEnabled
|
||||
VoicePushToTalkHotkey.shared.setEnabled(pttEnabled)
|
||||
await TalkModeRuntime.shared.setEnabled(enabled)
|
||||
// Resume voice wake listener *after* TalkMode audio is fully torn down.
|
||||
// Check swabbleEnabled (not voiceWakeTriggersTalkMode) so the paused wake listener
|
||||
@@ -27,8 +32,15 @@ final class TalkModeController {
|
||||
}
|
||||
|
||||
func updatePhase(_ phase: TalkModePhase) {
|
||||
let previousPhase = self.phase
|
||||
self.phase = phase
|
||||
TalkOverlayController.shared.updatePhase(phase)
|
||||
|
||||
// Play distinct system sounds for each phase transition.
|
||||
if phase != previousPhase {
|
||||
Self.playPhaseSound(phase, previousPhase: previousPhase)
|
||||
}
|
||||
|
||||
let effectivePhase = self.isPaused ? "paused" : phase.rawValue
|
||||
Task {
|
||||
await GatewayConnection.shared.talkMode(
|
||||
@@ -37,6 +49,25 @@ final class TalkModeController {
|
||||
}
|
||||
}
|
||||
|
||||
private static func playPhaseSound(_ phase: TalkModePhase, previousPhase: TalkModePhase) {
|
||||
guard AppStateStore.shared.talkPhaseSoundsEnabled else { return }
|
||||
let soundName: String? = switch phase {
|
||||
case .thinking:
|
||||
"Tink" // 생각 중: 짧고 가벼운 소리
|
||||
case .speaking:
|
||||
"Pop" // 대답 시작: 톡 소리
|
||||
case .listening:
|
||||
// 대답 중단(speaking→listening): 부드러운 종료음
|
||||
// 듣기 시작(thinking→listening 등): 잠수함 소리
|
||||
previousPhase == .speaking ? "Bottle" : "Submarine"
|
||||
case .idle:
|
||||
nil
|
||||
}
|
||||
if let soundName {
|
||||
NSSound(named: NSSound.Name(soundName))?.play()
|
||||
}
|
||||
}
|
||||
|
||||
func updateLevel(_ level: Double) {
|
||||
TalkOverlayController.shared.updateLevel(level)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ struct TalkModeGatewayConfigState {
|
||||
let outputFormat: String?
|
||||
let interruptOnSpeech: Bool
|
||||
let silenceTimeoutMs: Int
|
||||
let speechLocaleID: String?
|
||||
let apiKey: String?
|
||||
let seamColorHex: String?
|
||||
}
|
||||
@@ -53,6 +54,7 @@ enum TalkModeGatewayConfigParser {
|
||||
}
|
||||
let outputFormat = activeConfig?["outputFormat"]?.stringValue
|
||||
let interrupt = talk?["interruptOnSpeech"]?.boolValue
|
||||
let speechLocaleID = TalkConfigParsing.resolvedSpeechLocaleID(talk)
|
||||
let apiKey = activeConfig?["apiKey"]?.stringValue
|
||||
let resolvedVoice: String? = if activeProvider == defaultProvider {
|
||||
(voice?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false ? voice : nil) ??
|
||||
@@ -78,6 +80,7 @@ enum TalkModeGatewayConfigParser {
|
||||
outputFormat: outputFormat,
|
||||
interruptOnSpeech: interrupt ?? true,
|
||||
silenceTimeoutMs: silenceTimeoutMs,
|
||||
speechLocaleID: speechLocaleID,
|
||||
apiKey: resolvedApiKey,
|
||||
seamColorHex: rawSeam.isEmpty ? nil : rawSeam)
|
||||
}
|
||||
@@ -104,6 +107,7 @@ enum TalkModeGatewayConfigParser {
|
||||
outputFormat: nil,
|
||||
interruptOnSpeech: true,
|
||||
silenceTimeoutMs: defaultSilenceTimeoutMs,
|
||||
speechLocaleID: nil,
|
||||
apiKey: resolvedApiKey,
|
||||
seamColorHex: nil)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,7 @@ actor TalkModeRuntime {
|
||||
private var defaultOutputFormat: String?
|
||||
private var interruptOnSpeech: Bool = true
|
||||
private var activeTalkProvider = TalkModeRuntime.defaultTalkProvider
|
||||
private var speechLocaleID: String?
|
||||
private var lastInterruptedAtSeconds: Double?
|
||||
private var voiceAliases: [String: String] = [:]
|
||||
private var lastSpokenText: String?
|
||||
@@ -186,12 +187,23 @@ actor TalkModeRuntime {
|
||||
self.recognitionGeneration &+= 1
|
||||
let generation = self.recognitionGeneration
|
||||
|
||||
let locale = await MainActor.run { AppStateStore.shared.voiceWakeLocaleID }
|
||||
self.recognizer = SFSpeechRecognizer(locale: Locale(identifier: locale))
|
||||
let voiceWakeLocale = await MainActor.run { AppStateStore.shared.voiceWakeLocaleID }
|
||||
let supportedLocaleIDs = Set(SFSpeechRecognizer.supportedLocales().map(\.identifier))
|
||||
let localeID = TalkConfigParsing.resolvedSpeechRecognitionLocaleID(
|
||||
preferredLocaleIDs: [
|
||||
self.speechLocaleID,
|
||||
voiceWakeLocale,
|
||||
Locale.autoupdatingCurrent.identifier,
|
||||
],
|
||||
supportedLocaleIDs: supportedLocaleIDs)
|
||||
self.recognizer = localeID
|
||||
.map { SFSpeechRecognizer(locale: Locale(identifier: $0)) }
|
||||
?? SFSpeechRecognizer()
|
||||
guard let recognizer, recognizer.isAvailable else {
|
||||
self.logger.error("talk recognizer unavailable")
|
||||
return
|
||||
}
|
||||
self.logger.debug("talk recognizer locale=\(recognizer.locale.identifier, privacy: .public)")
|
||||
|
||||
let request = SFSpeechAudioBufferRecognitionRequest()
|
||||
Self.configureRecognitionRequest(request)
|
||||
@@ -1009,11 +1021,22 @@ extension TalkModeRuntime {
|
||||
self.defaultOutputFormat = cfg.outputFormat
|
||||
self.interruptOnSpeech = cfg.interruptOnSpeech
|
||||
self.activeTalkProvider = cfg.activeProvider
|
||||
self.silenceWindow = TimeInterval(cfg.silenceTimeoutMs) / 1000
|
||||
let configuredSilenceMs = cfg.silenceTimeoutMs
|
||||
let locale = await MainActor.run { AppStateStore.shared.voiceWakeLocaleID }
|
||||
let isCJKLocale = locale.hasPrefix("ko") || locale.hasPrefix("ja") || locale.hasPrefix("zh")
|
||||
let effectiveSilenceMs = isCJKLocale ? max(configuredSilenceMs, 2000) : configuredSilenceMs
|
||||
if isCJKLocale, configuredSilenceMs < 2000 {
|
||||
self.logger
|
||||
.info(
|
||||
"talk CJK locale: silence timeout clamped " +
|
||||
"\(configuredSilenceMs, privacy: .public)ms -> 2000ms")
|
||||
}
|
||||
self.silenceWindow = TimeInterval(effectiveSilenceMs) / 1000
|
||||
self.speechLocaleID = cfg.speechLocaleID
|
||||
self.apiKey = cfg.apiKey
|
||||
let hasApiKey = (cfg.apiKey?.isEmpty == false)
|
||||
let voiceLabel = (cfg.voiceId?.isEmpty == false) ? cfg.voiceId! : "none"
|
||||
let modelLabel = (cfg.modelId?.isEmpty == false) ? cfg.modelId! : "none"
|
||||
let voiceLabel = cfg.voiceId.flatMap { $0.isEmpty ? nil : $0 } ?? "none"
|
||||
let modelLabel = cfg.modelId.flatMap { $0.isEmpty ? nil : $0 } ?? "none"
|
||||
self.logger
|
||||
.info(
|
||||
"talk config provider=\(cfg.activeProvider, privacy: .public) " +
|
||||
@@ -1021,7 +1044,8 @@ extension TalkModeRuntime {
|
||||
"modelId=\(modelLabel, privacy: .public) " +
|
||||
"apiKey=\(hasApiKey, privacy: .public) " +
|
||||
"interrupt=\(cfg.interruptOnSpeech, privacy: .public) " +
|
||||
"silenceTimeoutMs=\(cfg.silenceTimeoutMs, privacy: .public)")
|
||||
"silenceTimeoutMs=\(cfg.silenceTimeoutMs, privacy: .public) " +
|
||||
"speechLocale=\(cfg.speechLocaleID ?? "device", privacy: .public)")
|
||||
}
|
||||
|
||||
static func selectTalkProviderConfig(
|
||||
|
||||
57
apps/macos/Sources/OpenClaw/TalkSpeechInterruptMonitor.swift
Normal file
57
apps/macos/Sources/OpenClaw/TalkSpeechInterruptMonitor.swift
Normal file
@@ -0,0 +1,57 @@
|
||||
import AppKit
|
||||
import OSLog
|
||||
|
||||
/// Monitors right Option key (keyCode 61) to interrupt Talk Mode speech.
|
||||
/// Independent of Push-to-Talk — active whenever Talk Mode is enabled.
|
||||
final class TalkSpeechInterruptMonitor: @unchecked Sendable {
|
||||
static let shared = TalkSpeechInterruptMonitor()
|
||||
|
||||
private let logger = Logger(subsystem: "ai.openclaw", category: "talk.interrupt")
|
||||
private var globalMonitor: Any?
|
||||
private var localMonitor: Any?
|
||||
|
||||
func setEnabled(_ enabled: Bool) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
if enabled {
|
||||
self.startMonitoring()
|
||||
} else {
|
||||
self.stopMonitoring()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startMonitoring() {
|
||||
guard self.globalMonitor == nil, self.localMonitor == nil else { return }
|
||||
self.globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
|
||||
self?.handleFlags(keyCode: event.keyCode, modifierFlags: event.modifierFlags)
|
||||
}
|
||||
self.localMonitor = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
|
||||
self?.handleFlags(keyCode: event.keyCode, modifierFlags: event.modifierFlags)
|
||||
return event
|
||||
}
|
||||
self.logger.info("talk interrupt monitor started")
|
||||
}
|
||||
|
||||
private func stopMonitoring() {
|
||||
if let globalMonitor {
|
||||
NSEvent.removeMonitor(globalMonitor)
|
||||
self.globalMonitor = nil
|
||||
}
|
||||
if let localMonitor {
|
||||
NSEvent.removeMonitor(localMonitor)
|
||||
self.localMonitor = nil
|
||||
}
|
||||
self.logger.info("talk interrupt monitor stopped")
|
||||
}
|
||||
|
||||
private func handleFlags(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags) {
|
||||
// Right Option key down (keyCode 61).
|
||||
guard keyCode == 61, modifierFlags.contains(.option) else { return }
|
||||
Task { @MainActor in
|
||||
guard TalkModeController.shared.phase == .speaking else { return }
|
||||
self.logger.info("right option — interrupting talk mode speech")
|
||||
TalkModeController.shared.stopSpeaking(reason: .userTap)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable {
|
||||
|
||||
private func updateModifierState(keyCode: UInt16, modifierFlags: NSEvent.ModifierFlags) {
|
||||
// assert(Thread.isMainThread) - Removed for Swift 6
|
||||
|
||||
// Right Option (keyCode 61) acts as a hold-to-talk modifier.
|
||||
if keyCode == 61 {
|
||||
self.optionDown = modifierFlags.contains(.option)
|
||||
|
||||
@@ -72,6 +72,31 @@ struct VoiceWakeSettings: View {
|
||||
binding: self.$state.voicePushToTalkEnabled)
|
||||
.disabled(!voiceWakeSupported)
|
||||
|
||||
if self.state.voicePushToTalkEnabled, self.state.talkEnabled {
|
||||
Text("Push-to-Talk is paused while Talk Mode is active. It resumes when Talk Mode is turned off.")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, 20)
|
||||
}
|
||||
|
||||
SettingsToggleRow(
|
||||
title: "Play phase-transition sounds",
|
||||
subtitle: """
|
||||
Play short system sounds when Talk Mode switches between
|
||||
listening, thinking, and speaking.
|
||||
""",
|
||||
binding: self.$state.talkPhaseSoundsEnabled)
|
||||
.disabled(!voiceWakeSupported)
|
||||
|
||||
SettingsToggleRow(
|
||||
title: "Press Right Option to stop speech",
|
||||
subtitle: """
|
||||
Tap the right Option key to interrupt the assistant while it is
|
||||
speaking and return to listening.
|
||||
""",
|
||||
binding: self.$state.talkShiftToStopEnabled)
|
||||
.disabled(!voiceWakeSupported)
|
||||
|
||||
if !voiceWakeSupported {
|
||||
Label("Voice Wake requires macOS 26 or newer.", systemImage: "exclamationmark.triangle.fill")
|
||||
.font(.callout)
|
||||
|
||||
@@ -25,10 +25,10 @@ struct TalkModeGatewayConfigTests {
|
||||
"voiceId": "unused-voice",
|
||||
],
|
||||
],
|
||||
"speechLocale": "ru-RU",
|
||||
]),
|
||||
],
|
||||
issues: nil
|
||||
)
|
||||
issues: nil)
|
||||
|
||||
let parsed = TalkModeGatewayConfigParser.parse(
|
||||
snapshot: snapshot,
|
||||
@@ -37,12 +37,12 @@ struct TalkModeGatewayConfigTests {
|
||||
defaultSilenceTimeoutMs: TalkDefaults.silenceTimeoutMs,
|
||||
envVoice: "env-voice",
|
||||
sagVoice: "sag-voice",
|
||||
envApiKey: "env-key"
|
||||
)
|
||||
envApiKey: "env-key")
|
||||
|
||||
#expect(parsed.activeProvider == "mlx")
|
||||
#expect(parsed.modelId == nil)
|
||||
#expect(parsed.apiKey == nil)
|
||||
#expect(parsed.voiceId == "unused-voice")
|
||||
#expect(parsed.speechLocaleID == "ru-RU")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,46 @@ public enum TalkConfigParsing {
|
||||
self.resolvedPositiveInt(talk?["silenceTimeoutMs"], fallback: fallback)
|
||||
}
|
||||
|
||||
public static func normalizedSpeechLocaleID(_ value: String?) -> String? {
|
||||
let trimmed = (value ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed.replacingOccurrences(of: "_", with: "-")
|
||||
}
|
||||
|
||||
public static func resolvedSpeechLocaleID(
|
||||
_ talk: [String: AnyCodable]?,
|
||||
fallback: String? = nil
|
||||
) -> String? {
|
||||
self.normalizedSpeechLocaleID(talk?["speechLocale"]?.stringValue)
|
||||
?? self.normalizedSpeechLocaleID(fallback)
|
||||
}
|
||||
|
||||
public static func normalizedExplicitSpeechLocaleID(
|
||||
_ value: String?,
|
||||
automaticID: String = "auto"
|
||||
) -> String? {
|
||||
let normalized = self.normalizedSpeechLocaleID(value)
|
||||
return normalized == automaticID ? nil : normalized
|
||||
}
|
||||
|
||||
public static func resolvedSpeechRecognitionLocaleID(
|
||||
preferredLocaleIDs: [String?],
|
||||
fallbackLocaleID: String = "en-US",
|
||||
supportedLocaleIDs: Set<String>
|
||||
) -> String? {
|
||||
let supported = Set(supportedLocaleIDs.compactMap(self.normalizedSpeechLocaleID))
|
||||
var seen = Set<String>()
|
||||
let candidates = (preferredLocaleIDs + [fallbackLocaleID])
|
||||
.compactMap(self.normalizedSpeechLocaleID)
|
||||
|
||||
for candidate in candidates {
|
||||
guard seen.insert(candidate).inserted else { continue }
|
||||
if supported.isEmpty || supported.contains(candidate) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func normalizedTalkProviderID(_ raw: String?) -> String? {
|
||||
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
|
||||
@@ -116,4 +116,21 @@ struct TalkConfigParsingTests {
|
||||
#expect(TalkConfigParsing.resolvedPositiveInt(AnyCodable(true), fallback: 700) == 700)
|
||||
#expect(TalkConfigParsing.resolvedPositiveInt(AnyCodable("1500"), fallback: 700) == 700)
|
||||
}
|
||||
|
||||
@Test func resolvesSpeechLocaleID() {
|
||||
#expect(TalkConfigParsing.resolvedSpeechLocaleID(["speechLocale": AnyCodable(" ru_RU ")]) == "ru-RU")
|
||||
#expect(TalkConfigParsing.resolvedSpeechLocaleID(["speechLocale": AnyCodable("")], fallback: "en-US") == "en-US")
|
||||
}
|
||||
|
||||
@Test func resolvesSpeechRecognitionLocaleFromSupportedFallbacks() {
|
||||
let locale = TalkConfigParsing.resolvedSpeechRecognitionLocaleID(
|
||||
preferredLocaleIDs: ["zz-ZZ", "fr-FR"],
|
||||
supportedLocaleIDs: ["fr-FR", "en-US"])
|
||||
let fallback = TalkConfigParsing.resolvedSpeechRecognitionLocaleID(
|
||||
preferredLocaleIDs: ["zz-ZZ", "yy-YY"],
|
||||
supportedLocaleIDs: ["en-US"])
|
||||
|
||||
#expect(locale == "fr-FR")
|
||||
#expect(fallback == "en-US")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
9a012a9c87b9010683289dc7d68ba5446a4b78beedf381e2c5f9d486f25a9213 config-baseline.json
|
||||
6128d6eff8c28d17194d1ae9ee7f72abae48da1c6476ab16e6378f1898e4373a config-baseline.core.json
|
||||
3efb041739877bd5387ffc87e0ddd11be43d80d38e7779407ce8091dcb797e5e config-baseline.json
|
||||
5c6e35c5846f654d717d4b20853649e0b45a746423834f539b2a2223abcd5226 config-baseline.core.json
|
||||
7cd9c908f066c143eab2a201efbc9640f483ab28bba92ddeca1d18cc2b528bc3 config-baseline.channel.json
|
||||
7825b56a5b3fcdbe2e09ef8fe5d9f12ac3598435afebe20413051e45b0d1968e config-baseline.plugin.json
|
||||
a5479c182ec987bb21e814b8a4e7b3bda7190ae5c2b35fd5ca403dfa48afa115 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
d5bad55d588ecafab1298a2a79578ce13becced8bc33d2b8543161ab528feca4 plugin-sdk-api-baseline.json
|
||||
373ded33d5ecc61229de5179827182f0c6f805a804e1f0666cf2da68301153be plugin-sdk-api-baseline.jsonl
|
||||
690c1cd4c0c2c3d31577958120e14ac0bf555af529e03aa5e7965b1d04659c49 plugin-sdk-api-baseline.json
|
||||
a0e6ba472ddd3acea34c0a8fda8cbb7d1172b1671a671d5fef5a9f42d749ce0d plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
"source": "OpenAI provider",
|
||||
"target": "OpenAI provider"
|
||||
},
|
||||
{
|
||||
"source": "Azure Speech",
|
||||
"target": "Azure Speech"
|
||||
},
|
||||
{
|
||||
"source": "Azure Speech provider",
|
||||
"target": "Azure Speech provider"
|
||||
},
|
||||
{
|
||||
"source": "Status",
|
||||
"target": "Status"
|
||||
@@ -111,6 +119,10 @@
|
||||
"source": "BytePlus (International)",
|
||||
"target": "BytePlus(国际版)"
|
||||
},
|
||||
{
|
||||
"source": "Volcengine TTS HTTP API",
|
||||
"target": "Volcengine TTS HTTP API"
|
||||
},
|
||||
{
|
||||
"source": "Amazon Bedrock Mantle",
|
||||
"target": "Amazon Bedrock Mantle"
|
||||
|
||||
@@ -84,7 +84,7 @@ This fires ~5–6 times per month instead of 0–1 times per month. OpenClaw use
|
||||
| Current session | `current` | Bound at creation time | Context-aware recurring work |
|
||||
| Custom session | `session:custom-id` | Persistent named session | Workflows that build on history |
|
||||
|
||||
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
|
||||
**Main session** jobs enqueue a system event and optionally wake the heartbeat (`--wake now` or `--wake next-heartbeat`). Those system events do not extend daily/idle reset freshness for the target session. **Isolated** jobs run a dedicated agent turn with a fresh session. **Custom sessions** (`session:xxx`) persist context across runs, enabling workflows like daily standups that build on previous summaries.
|
||||
|
||||
For isolated jobs, “fresh session” means a new transcript/session id for each run. OpenClaw may carry safe preferences such as thinking/fast/verbose settings, labels, and explicit user-selected model/auth overrides, but it does not inherit ambient conversation context from an older cron row: channel/group routing, send or queue policy, elevation, origin, or ACP runtime binding. Use `current` or `session:<id>` when a recurring job should deliberately build on the same conversation context.
|
||||
|
||||
@@ -140,7 +140,7 @@ forever.
|
||||
| `webhook` | POST finished event payload to a URL |
|
||||
| `none` | No runner fallback delivery |
|
||||
|
||||
Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:<id>`, `user:<id>`).
|
||||
Use `--announce --channel telegram --to "-1001234567890"` for channel delivery. For Telegram forum topics, use `-1001234567890:topic:123`. Slack/Discord/Mattermost targets should use explicit prefixes (`channel:<id>`, `user:<id>`). Matrix room IDs are case-sensitive; use the exact room ID or `room:!room:server` form from Matrix.
|
||||
|
||||
For isolated jobs, chat delivery is shared. If a chat route is available, the
|
||||
agent can use the `message` tool even when the job uses `--no-deliver`. If the
|
||||
@@ -148,6 +148,11 @@ agent sends to the configured/current target, OpenClaw skips the fallback
|
||||
announce. Otherwise `announce`, `webhook`, and `none` only control what the
|
||||
runner does with the final reply after the agent turn.
|
||||
|
||||
When an agent creates an isolated reminder from an active chat, OpenClaw stores
|
||||
the preserved live delivery target for the fallback announce route. Internal
|
||||
session keys may be lowercase; provider delivery targets are not reconstructed
|
||||
from those keys when current chat context is available.
|
||||
|
||||
Failure notifications follow a separate destination path:
|
||||
|
||||
- `cron.failureDestination` sets a global default for failure notifications.
|
||||
@@ -418,6 +423,9 @@ openclaw doctor
|
||||
- Delivery mode `none` means no runner fallback send is expected. The agent can
|
||||
still send directly with the `message` tool when a chat route is available.
|
||||
- Delivery target missing/invalid (`channel`/`to`) means outbound was skipped.
|
||||
- For Matrix, copied or legacy jobs with lowercased `delivery.to` room IDs can
|
||||
fail because Matrix room IDs are case-sensitive. Edit the job to the exact
|
||||
`!room:server` or `room:!room:server` value from Matrix.
|
||||
- Channel auth errors (`unauthorized`, `Forbidden`) mean delivery was blocked by credentials.
|
||||
- If the isolated run returns only the silent token (`NO_REPLY` / `no_reply`),
|
||||
OpenClaw suppresses direct outbound delivery and also suppresses the fallback
|
||||
@@ -425,6 +433,18 @@ openclaw doctor
|
||||
- If the agent should message the user itself, check that the job has a usable
|
||||
route (`channel: "last"` with a previous chat, or an explicit channel/target).
|
||||
|
||||
### Cron or heartbeat appears to prevent `/new`-style rollover
|
||||
|
||||
- Daily and idle reset freshness is not based on `updatedAt`; see
|
||||
[Session management](/concepts/session#session-lifecycle).
|
||||
- Cron wakeups, heartbeat runs, exec notifications, and gateway bookkeeping may
|
||||
update the session row for routing/status, but they do not extend
|
||||
`sessionStartedAt` or `lastInteractionAt`.
|
||||
- For legacy rows created before those fields existed, OpenClaw can recover
|
||||
`sessionStartedAt` from the transcript JSONL session header when the file is
|
||||
still available. Legacy idle rows without `lastInteractionAt` use that
|
||||
recovered start time as their idle baseline.
|
||||
|
||||
### Timezone gotchas
|
||||
|
||||
- Cron without `--tz` uses the gateway host timezone.
|
||||
|
||||
@@ -126,6 +126,11 @@ Each event includes: `type`, `action`, `sessionKey`, `timestamp`, `messages` (pu
|
||||
|
||||
**Compaction events**: `session:compact:before` includes `messageCount`, `tokenCount`. `session:compact:after` adds `compactedCount`, `summaryLength`, `tokensBefore`, `tokensAfter`.
|
||||
|
||||
`command:stop` observes the user issuing `/stop`; it is cancellation/command
|
||||
lifecycle, not an agent-finalization gate. Plugins that need to inspect a
|
||||
natural final answer and ask the agent for one more pass should use the typed
|
||||
plugin hook `before_agent_finalize` instead. See [Plugin hooks](/plugins/hooks).
|
||||
|
||||
## Hook discovery
|
||||
|
||||
Hooks are discovered from these directories, in order of increasing override precedence:
|
||||
|
||||
@@ -93,7 +93,7 @@ See [Hooks](/automation/hooks).
|
||||
|
||||
### Heartbeat
|
||||
|
||||
Heartbeat is a periodic main-session turn (default every 30 minutes). It batches multiple checks (inbox, calendar, notifications) in one agent turn with full session context. Heartbeat turns do not create task records. Use `HEARTBEAT.md` for a small checklist, or a `tasks:` block when you want due-only periodic checks inside heartbeat itself. Empty heartbeat files skip as `empty-heartbeat-file`; due-only task mode skips as `no-tasks-due`.
|
||||
Heartbeat is a periodic main-session turn (default every 30 minutes). It batches multiple checks (inbox, calendar, notifications) in one agent turn with full session context. Heartbeat turns do not create task records and do not extend daily/idle session reset freshness. Use `HEARTBEAT.md` for a small checklist, or a `tasks:` block when you want due-only periodic checks inside heartbeat itself. Empty heartbeat files skip as `empty-heartbeat-file`; due-only task mode skips as `no-tasks-due`.
|
||||
|
||||
See [Heartbeat](/gateway/heartbeat).
|
||||
|
||||
|
||||
@@ -398,6 +398,12 @@ Restore room keys from server backup:
|
||||
openclaw matrix verify backup restore
|
||||
```
|
||||
|
||||
If the backup key is not already loaded on disk, pass the Matrix recovery key:
|
||||
|
||||
```bash
|
||||
openclaw matrix verify backup restore --recovery-key "<your-recovery-key>"
|
||||
```
|
||||
|
||||
Interactive self-verification flow:
|
||||
|
||||
```bash
|
||||
@@ -480,6 +486,8 @@ openclaw matrix verify status
|
||||
```
|
||||
|
||||
Add `--account <id>` to target a named account. This can also recreate secret storage if the current backup secret cannot be loaded safely.
|
||||
Add `--rotate-recovery-key` only when you intentionally want the old recovery
|
||||
key to stop unlocking the fresh backup baseline.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -501,6 +509,34 @@ openclaw matrix verify status
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Deleted or invalid Matrix device">
|
||||
If `verify status` says the current device is no longer listed on the
|
||||
homeserver, create a new OpenClaw Matrix device. For password login:
|
||||
|
||||
```bash
|
||||
openclaw matrix account add \
|
||||
--account assistant \
|
||||
--homeserver https://matrix.example.org \
|
||||
--user-id '@assistant:example.org' \
|
||||
--password '<password>' \
|
||||
--device-name OpenClaw-Gateway
|
||||
```
|
||||
|
||||
For token auth, create a fresh access token in your Matrix client or admin UI,
|
||||
then update OpenClaw:
|
||||
|
||||
```bash
|
||||
openclaw matrix account add \
|
||||
--account assistant \
|
||||
--homeserver https://matrix.example.org \
|
||||
--access-token '<token>'
|
||||
```
|
||||
|
||||
Replace `assistant` with the account ID from the failed command, or omit
|
||||
`--account` for the default account.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Device hygiene">
|
||||
Old OpenClaw-managed devices can accumulate. List and prune:
|
||||
|
||||
@@ -847,6 +883,11 @@ Matrix accepts these target forms anywhere OpenClaw asks you for a room or user
|
||||
- Rooms: `!room:server`, `room:!room:server`, or `matrix:room:!room:server`
|
||||
- Aliases: `#alias:server`, `channel:#alias:server`, or `matrix:channel:#alias:server`
|
||||
|
||||
Matrix room IDs are case-sensitive. Use the exact room ID casing from Matrix
|
||||
when configuring explicit delivery targets, cron jobs, bindings, or allowlists.
|
||||
OpenClaw keeps internal session keys canonical for storage, so those lowercase
|
||||
keys are not a reliable source for Matrix delivery IDs.
|
||||
|
||||
Live directory lookup uses the logged-in Matrix account:
|
||||
|
||||
- User lookups query the Matrix user directory on that homeserver.
|
||||
|
||||
@@ -5,7 +5,7 @@ read_when:
|
||||
title: "Microsoft Teams"
|
||||
---
|
||||
|
||||
Text and DM attachments are supported; channel and group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards. Message actions expose explicit `upload-file` for file-first sends.
|
||||
Status: text + DM attachments are supported; channel/group file sending requires `sharePointSiteId` + Graph permissions (see [Sending files in group chats](#sending-files-in-group-chats)). Polls are sent via Adaptive Cards. Message actions expose explicit `upload-file` for file-first sends.
|
||||
|
||||
## Bundled plugin
|
||||
|
||||
@@ -27,25 +27,64 @@ openclaw plugins install ./path/to/local/msteams-plugin
|
||||
|
||||
Details: [Plugins](/tools/plugin)
|
||||
|
||||
## Quick setup (beginner)
|
||||
## Quick setup
|
||||
|
||||
1. Ensure the Microsoft Teams plugin is available.
|
||||
- Current packaged OpenClaw releases already bundle it.
|
||||
- Older/custom installs can add it manually with the commands above.
|
||||
2. Create an **Azure Bot** (App ID + client secret + tenant ID).
|
||||
3. Configure OpenClaw with those credentials.
|
||||
4. Expose `/api/messages` (port 3978 by default) via a public URL or tunnel.
|
||||
5. Install the Teams app package and start the gateway.
|
||||
The [`@microsoft/teams.cli`](https://www.npmjs.com/package/@microsoft/teams.cli) handles bot registration, manifest creation, and credential generation in a single command.
|
||||
|
||||
Minimal config (client secret):
|
||||
**1. Install and log in**
|
||||
|
||||
```bash
|
||||
npm install -g @microsoft/teams.cli@preview
|
||||
teams login
|
||||
teams status # verify you're logged in and see your tenant info
|
||||
```
|
||||
|
||||
> **Note:** The Teams CLI is currently in preview. Commands and flags may change between releases.
|
||||
|
||||
**2. Start a tunnel** (Teams can't reach localhost)
|
||||
|
||||
Install and authenticate the devtunnel CLI if you haven't already ([getting started guide](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/get-started)).
|
||||
|
||||
```bash
|
||||
# One-time setup (persistent URL across sessions):
|
||||
devtunnel create my-openclaw-bot --allow-anonymous
|
||||
devtunnel port create my-openclaw-bot -p 3978 --protocol auto
|
||||
|
||||
# Each dev session:
|
||||
devtunnel host my-openclaw-bot
|
||||
# Your endpoint: https://<tunnel-id>.devtunnels.ms/api/messages
|
||||
```
|
||||
|
||||
> **Note:** `--allow-anonymous` is required because Teams can't authenticate with devtunnels. Each incoming bot request is still validated by the Teams SDK automatically.
|
||||
|
||||
Alternatives: `ngrok http 3978` or `tailscale funnel 3978` (but these may change URLs each session).
|
||||
|
||||
**3. Create the app**
|
||||
|
||||
```bash
|
||||
teams app create \
|
||||
--name "OpenClaw" \
|
||||
--endpoint "https://<your-tunnel-url>/api/messages"
|
||||
```
|
||||
|
||||
This single command:
|
||||
|
||||
- Creates an Entra ID (Azure AD) application
|
||||
- Generates a client secret
|
||||
- Builds and uploads a Teams app manifest (with icons)
|
||||
- Registers the bot (Teams-managed by default — no Azure subscription needed)
|
||||
|
||||
The output will show `CLIENT_ID`, `CLIENT_SECRET`, `TENANT_ID`, and a **Teams App ID** — note these for the next steps. It also offers to install the app in Teams directly.
|
||||
|
||||
**4. Configure OpenClaw** using the credentials from the output:
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
msteams: {
|
||||
enabled: true,
|
||||
appId: "<APP_ID>",
|
||||
appPassword: "<APP_PASSWORD>",
|
||||
appId: "<CLIENT_ID>",
|
||||
appPassword: "<CLIENT_SECRET>",
|
||||
tenantId: "<TENANT_ID>",
|
||||
webhook: { port: 3978, path: "/api/messages" },
|
||||
},
|
||||
@@ -53,10 +92,34 @@ Minimal config (client secret):
|
||||
}
|
||||
```
|
||||
|
||||
For production deployments, consider using [federated authentication](#federated-authentication) (certificate or managed identity) instead of client secrets.
|
||||
Or use environment variables directly: `MSTEAMS_APP_ID`, `MSTEAMS_APP_PASSWORD`, `MSTEAMS_TENANT_ID`.
|
||||
|
||||
**5. Install the app in Teams**
|
||||
|
||||
`teams app create` will prompt you to install the app — select "Install in Teams". If you skipped it, you can get the link later:
|
||||
|
||||
```bash
|
||||
teams app get <teamsAppId> --install-link
|
||||
```
|
||||
|
||||
**6. Verify everything works**
|
||||
|
||||
```bash
|
||||
teams app doctor <teamsAppId>
|
||||
```
|
||||
|
||||
This runs diagnostics across bot registration, AAD app config, manifest validity, and SSO setup.
|
||||
|
||||
For production deployments, consider using [federated authentication](#federated-authentication-certificate--managed-identity) (certificate or managed identity) instead of client secrets.
|
||||
|
||||
Note: group chats are blocked by default (`channels.msteams.groupPolicy: "allowlist"`). To allow group replies, set `channels.msteams.groupAllowFrom` (or use `groupPolicy: "open"` to allow any member, mention-gated).
|
||||
|
||||
## Goals
|
||||
|
||||
- Talk to OpenClaw via Teams DMs, group chats, or channels.
|
||||
- Keep routing deterministic: replies always go back to the channel they arrived on.
|
||||
- Default to safe channel behavior (mentions required unless configured otherwise).
|
||||
|
||||
## Config writes
|
||||
|
||||
By default, Microsoft Teams is allowed to write config updates triggered by `/config set|unset` (requires `commands.config: true`).
|
||||
@@ -126,54 +189,93 @@ Example:
|
||||
}
|
||||
```
|
||||
|
||||
## Azure Bot setup
|
||||
<details>
|
||||
<summary><strong>Manual setup (without the Teams CLI)</strong></summary>
|
||||
|
||||
Before configuring OpenClaw, create an Azure Bot resource and capture its credentials.
|
||||
If you can't use the Teams CLI, you can set up the bot manually through the Azure Portal.
|
||||
|
||||
<Steps>
|
||||
<Step title="Create the Azure Bot">
|
||||
Go to [Create Azure Bot](https://portal.azure.com/#create/Microsoft.AzureBot) and fill in the **Basics** tab:
|
||||
### How it works
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------- |
|
||||
| **Bot handle** | Your bot name, e.g. `openclaw-msteams` (must be unique) |
|
||||
| **Subscription** | Your Azure subscription |
|
||||
| **Resource group** | Create new or use existing |
|
||||
| **Pricing tier** | **Free** for dev/testing |
|
||||
| **Type of App** | **Single Tenant** (recommended) |
|
||||
| **Creation type** | **Create new Microsoft App ID** |
|
||||
1. Ensure the Microsoft Teams plugin is available (bundled in current releases).
|
||||
2. Create an **Azure Bot** (App ID + secret + tenant ID).
|
||||
3. Build a **Teams app package** that references the bot and includes the RSC permissions below.
|
||||
4. Upload/install the Teams app into a team (or personal scope for DMs).
|
||||
5. Configure `msteams` in `~/.openclaw/openclaw.json` (or env vars) and start the gateway.
|
||||
6. The gateway listens for Bot Framework webhook traffic on `/api/messages` by default.
|
||||
|
||||
<Note>
|
||||
New multi-tenant bots were deprecated after 2025-07-31. Use **Single Tenant** for new bots.
|
||||
</Note>
|
||||
### Step 1: Create Azure Bot
|
||||
|
||||
Click **Review + create** → **Create** (wait ~1-2 minutes).
|
||||
1. Go to [Create Azure Bot](https://portal.azure.com/#create/Microsoft.AzureBot)
|
||||
2. Fill in the **Basics** tab:
|
||||
|
||||
</Step>
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------- |
|
||||
| **Bot handle** | Your bot name, e.g., `openclaw-msteams` (must be unique) |
|
||||
| **Subscription** | Select your Azure subscription |
|
||||
| **Resource group** | Create new or use existing |
|
||||
| **Pricing tier** | **Free** for dev/testing |
|
||||
| **Type of App** | **Single Tenant** (recommended - see note below) |
|
||||
| **Creation type** | **Create new Microsoft App ID** |
|
||||
|
||||
<Step title="Capture credentials">
|
||||
From the Azure Bot resource → **Configuration**:
|
||||
> **Deprecation notice:** Creation of new multi-tenant bots was deprecated after 2025-07-31. Use **Single Tenant** for new bots.
|
||||
|
||||
- copy **Microsoft App ID** → `appId`
|
||||
- **Manage Password** → **Certificates & secrets** → **New client secret** → copy the value → `appPassword`
|
||||
- **Overview** → **Directory (tenant) ID** → `tenantId`
|
||||
3. Click **Review + create** → **Create** (wait ~1-2 minutes)
|
||||
|
||||
</Step>
|
||||
### Step 2: Get Credentials
|
||||
|
||||
<Step title="Configure messaging endpoint">
|
||||
Azure Bot → **Configuration** → set **Messaging endpoint**:
|
||||
1. Go to your Azure Bot resource → **Configuration**
|
||||
2. Copy **Microsoft App ID** → this is your `appId`
|
||||
3. Click **Manage Password** → go to the App Registration
|
||||
4. Under **Certificates & secrets** → **New client secret** → copy the **Value** → this is your `appPassword`
|
||||
5. Go to **Overview** → copy **Directory (tenant) ID** → this is your `tenantId`
|
||||
|
||||
- Production: `https://your-domain.com/api/messages`
|
||||
- Local dev: use a tunnel (see [Local development](#local-development-tunneling))
|
||||
### Step 3: Configure Messaging Endpoint
|
||||
|
||||
</Step>
|
||||
1. In Azure Bot → **Configuration**
|
||||
2. Set **Messaging endpoint** to your webhook URL:
|
||||
- Production: `https://your-domain.com/api/messages`
|
||||
- Local dev: Use a tunnel (see [Local Development](#local-development-tunneling) below)
|
||||
|
||||
<Step title="Enable the Teams channel">
|
||||
Azure Bot → **Channels** → click **Microsoft Teams** → Configure → Save. Accept the Terms of Service.
|
||||
</Step>
|
||||
</Steps>
|
||||
### Step 4: Enable Teams Channel
|
||||
|
||||
## Federated authentication
|
||||
1. In Azure Bot → **Channels**
|
||||
2. Click **Microsoft Teams** → Configure → Save
|
||||
3. Accept the Terms of Service
|
||||
|
||||
### Step 5: Build Teams App Manifest
|
||||
|
||||
- Include a `bot` entry with `botId = <App ID>`.
|
||||
- Scopes: `personal`, `team`, `groupChat`.
|
||||
- `supportsFiles: true` (required for personal scope file handling).
|
||||
- Add RSC permissions (see [RSC Permissions](#current-teams-rsc-permissions-manifest)).
|
||||
- Create icons: `outline.png` (32x32) and `color.png` (192x192).
|
||||
- Zip all three files together: `manifest.json`, `outline.png`, `color.png`.
|
||||
|
||||
### Step 6: Configure OpenClaw
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
msteams: {
|
||||
enabled: true,
|
||||
appId: "<APP_ID>",
|
||||
appPassword: "<APP_PASSWORD>",
|
||||
tenantId: "<TENANT_ID>",
|
||||
webhook: { port: 3978, path: "/api/messages" },
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Environment variables: `MSTEAMS_APP_ID`, `MSTEAMS_APP_PASSWORD`, `MSTEAMS_TENANT_ID`.
|
||||
|
||||
### Step 7: Run the Gateway
|
||||
|
||||
The Teams channel starts automatically when the plugin is available and `msteams` config exists with credentials.
|
||||
|
||||
</details>
|
||||
|
||||
## Federated Authentication (Certificate + Managed Identity)
|
||||
|
||||
> Added in 2026.3.24
|
||||
|
||||
@@ -268,7 +370,7 @@ Use Azure Managed Identity for passwordless authentication. This is ideal for de
|
||||
- `MSTEAMS_USE_MANAGED_IDENTITY=true`
|
||||
- `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID=<client-id>` (only for user-assigned)
|
||||
|
||||
### AKS workload identity setup
|
||||
### AKS Workload Identity Setup
|
||||
|
||||
For AKS deployments using workload identity:
|
||||
|
||||
@@ -315,63 +417,55 @@ For AKS deployments using workload identity:
|
||||
|
||||
**Default behavior:** When `authType` is not set, OpenClaw defaults to client secret authentication. Existing configurations continue to work without changes.
|
||||
|
||||
## Local development (tunneling)
|
||||
## Local Development (Tunneling)
|
||||
|
||||
Teams can't reach `localhost`. Use a tunnel for local development:
|
||||
|
||||
**Option A: ngrok**
|
||||
Teams can't reach `localhost`. Use a persistent dev tunnel so your URL stays the same across sessions:
|
||||
|
||||
```bash
|
||||
ngrok http 3978
|
||||
# Copy the https URL, e.g., https://abc123.ngrok.io
|
||||
# Set messaging endpoint to: https://abc123.ngrok.io/api/messages
|
||||
# One-time setup:
|
||||
devtunnel create my-openclaw-bot --allow-anonymous
|
||||
devtunnel port create my-openclaw-bot -p 3978 --protocol auto
|
||||
|
||||
# Each dev session:
|
||||
devtunnel host my-openclaw-bot
|
||||
```
|
||||
|
||||
**Option B: Tailscale Funnel**
|
||||
Alternatives: `ngrok http 3978` or `tailscale funnel 3978` (URLs may change each session).
|
||||
|
||||
If your tunnel URL changes, update the endpoint:
|
||||
|
||||
```bash
|
||||
tailscale funnel 3978
|
||||
# Use your Tailscale funnel URL as the messaging endpoint
|
||||
teams app update <teamsAppId> --endpoint "https://<new-url>/api/messages"
|
||||
```
|
||||
|
||||
## Teams Developer Portal (alternative)
|
||||
## Testing the Bot
|
||||
|
||||
Instead of manually creating a manifest ZIP, you can use the [Teams Developer Portal](https://dev.teams.microsoft.com/apps):
|
||||
**Run diagnostics:**
|
||||
|
||||
1. Click **+ New app**
|
||||
2. Fill in basic info (name, description, developer info)
|
||||
3. Go to **App features** → **Bot**
|
||||
4. Select **Enter a bot ID manually** and paste your Azure Bot App ID
|
||||
5. Check scopes: **Personal**, **Team**, **Group Chat**
|
||||
6. Click **Distribute** → **Download app package**
|
||||
7. In Teams: **Apps** → **Manage your apps** → **Upload a custom app** → select the ZIP
|
||||
```bash
|
||||
teams app doctor <teamsAppId>
|
||||
```
|
||||
|
||||
This is often easier than hand-editing JSON manifests.
|
||||
Checks bot registration, AAD app, manifest, and SSO configuration in one pass.
|
||||
|
||||
## Testing the bot
|
||||
**Send a test message:**
|
||||
|
||||
**Option A: Azure Web Chat (verify webhook first)**
|
||||
|
||||
1. In Azure Portal → your Azure Bot resource → **Test in Web Chat**
|
||||
2. Send a message - you should see a response
|
||||
3. This confirms your webhook endpoint works before Teams setup
|
||||
|
||||
**Option B: Teams (after app installation)**
|
||||
|
||||
1. Install the Teams app (sideload or org catalog)
|
||||
1. Install the Teams app (use the install link from `teams app get <id> --install-link`)
|
||||
2. Find the bot in Teams and send a DM
|
||||
3. Check gateway logs for incoming activity
|
||||
|
||||
<Accordion title="Environment variable overrides">
|
||||
## Environment variables
|
||||
|
||||
Any of the bot/auth config keys can also be set via env vars:
|
||||
All config keys can be set via environment variables instead:
|
||||
|
||||
- `MSTEAMS_APP_ID`, `MSTEAMS_APP_PASSWORD`, `MSTEAMS_TENANT_ID`
|
||||
- `MSTEAMS_AUTH_TYPE` (`"secret"` or `"federated"`)
|
||||
- `MSTEAMS_CERTIFICATE_PATH`, `MSTEAMS_CERTIFICATE_THUMBPRINT` (federated + certificate)
|
||||
- `MSTEAMS_USE_MANAGED_IDENTITY`, `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID` (federated + managed identity; client ID only for user-assigned)
|
||||
|
||||
</Accordion>
|
||||
- `MSTEAMS_APP_ID`
|
||||
- `MSTEAMS_APP_PASSWORD`
|
||||
- `MSTEAMS_TENANT_ID`
|
||||
- `MSTEAMS_AUTH_TYPE` (optional: `"secret"` or `"federated"`)
|
||||
- `MSTEAMS_CERTIFICATE_PATH` (federated + certificate)
|
||||
- `MSTEAMS_CERTIFICATE_THUMBPRINT` (optional, not required for auth)
|
||||
- `MSTEAMS_USE_MANAGED_IDENTITY` (federated + managed identity)
|
||||
- `MSTEAMS_MANAGED_IDENTITY_CLIENT_ID` (user-assigned MI only)
|
||||
|
||||
## Member info action
|
||||
|
||||
@@ -393,7 +487,7 @@ The action is gated by `channels.msteams.actions.memberInfo` (default: enabled w
|
||||
- In other words, allowlists gate who can trigger the agent; only specific supplemental context paths are filtered today.
|
||||
- DM history can be limited with `channels.msteams.dmHistoryLimit` (user turns). Per-user overrides: `channels.msteams.dms["<user_id>"].historyLimit`.
|
||||
|
||||
## Current Teams RSC permissions
|
||||
## Current Teams RSC Permissions (Manifest)
|
||||
|
||||
These are the **existing resourceSpecific permissions** in our Teams app manifest. They only apply inside the team/chat where the app is installed.
|
||||
|
||||
@@ -411,7 +505,13 @@ These are the **existing resourceSpecific permissions** in our Teams app manifes
|
||||
|
||||
- `ChatMessage.Read.Chat` (Application) - receive all group chat messages without @mention
|
||||
|
||||
## Example Teams manifest
|
||||
To add RSC permissions via the Teams CLI:
|
||||
|
||||
```bash
|
||||
teams app rsc add <teamsAppId> ChannelMessage.Read.Group --type Application
|
||||
```
|
||||
|
||||
## Example Teams Manifest (redacted)
|
||||
|
||||
Minimal, valid example with the required fields. Replace IDs and URLs.
|
||||
|
||||
@@ -473,18 +573,31 @@ Minimal, valid example with the required fields. Replace IDs and URLs.
|
||||
|
||||
To update an already-installed Teams app (e.g., to add RSC permissions):
|
||||
|
||||
```bash
|
||||
# Download, edit, and re-upload the manifest
|
||||
teams app manifest download <teamsAppId> manifest.json
|
||||
# Edit manifest.json locally...
|
||||
teams app manifest upload manifest.json <teamsAppId>
|
||||
# Version is auto-bumped if content changed
|
||||
```
|
||||
|
||||
After updating, reinstall the app in each team for new permissions to take effect, and **fully quit and relaunch Teams** (not just close the window) to clear cached app metadata.
|
||||
|
||||
<details>
|
||||
<summary>Manual manifest update (without CLI)</summary>
|
||||
|
||||
1. Update your `manifest.json` with the new settings
|
||||
2. **Increment the `version` field** (e.g., `1.0.0` → `1.1.0`)
|
||||
3. **Re-zip** the manifest with icons (`manifest.json`, `outline.png`, `color.png`)
|
||||
4. Upload the new zip:
|
||||
- **Option A (Teams Admin Center):** Teams Admin Center → Teams apps → Manage apps → find your app → Upload new version
|
||||
- **Option B (Sideload):** In Teams → Apps → Manage your apps → Upload a custom app
|
||||
5. **For team channels:** Reinstall the app in each team for new permissions to take effect
|
||||
6. **Fully quit and relaunch Teams** (not just close the window) to clear cached app metadata
|
||||
- **Teams Admin Center:** Teams apps → Manage apps → find your app → Upload new version
|
||||
- **Sideload:** In Teams → Apps → Manage your apps → Upload a custom app
|
||||
|
||||
</details>
|
||||
|
||||
## Capabilities: RSC only vs Graph
|
||||
|
||||
### Teams RSC only (no Graph API permissions)
|
||||
### With **Teams RSC only** (app installed, no Graph API permissions)
|
||||
|
||||
Works:
|
||||
|
||||
@@ -498,7 +611,7 @@ Does NOT work:
|
||||
- Downloading attachments stored in SharePoint/OneDrive.
|
||||
- Reading message history (beyond the live webhook event).
|
||||
|
||||
### Teams RSC plus Microsoft Graph application permissions
|
||||
### With **Teams RSC + Microsoft Graph Application permissions**
|
||||
|
||||
Adds:
|
||||
|
||||
@@ -530,7 +643,7 @@ If you need images/files in **channels** or want to fetch **message history**, y
|
||||
|
||||
**Additional permission for user mentions:** User @mentions work out of the box for users in the conversation. However, if you want to dynamically search and mention users who are **not in the current conversation**, add `User.Read.All` (Application) permission and grant admin consent.
|
||||
|
||||
## Known limitations
|
||||
## Known Limitations
|
||||
|
||||
### Webhook timeouts
|
||||
|
||||
@@ -552,53 +665,40 @@ Teams markdown is more limited than Slack or Discord:
|
||||
|
||||
## Configuration
|
||||
|
||||
Grouped settings (see `/gateway/configuration` for shared channel patterns).
|
||||
Key settings (see `/gateway/configuration` for shared channel patterns):
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Core and webhook">
|
||||
- `channels.msteams.enabled`
|
||||
- `channels.msteams.appId`, `appPassword`, `tenantId`: bot credentials
|
||||
- `channels.msteams.webhook.port` (default `3978`)
|
||||
- `channels.msteams.webhook.path` (default `/api/messages`)
|
||||
</Accordion>
|
||||
- `channels.msteams.enabled`: enable/disable the channel.
|
||||
- `channels.msteams.appId`, `channels.msteams.appPassword`, `channels.msteams.tenantId`: bot credentials.
|
||||
- `channels.msteams.webhook.port` (default `3978`)
|
||||
- `channels.msteams.webhook.path` (default `/api/messages`)
|
||||
- `channels.msteams.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
|
||||
- `channels.msteams.allowFrom`: DM allowlist (AAD object IDs recommended). The wizard resolves names to IDs during setup when Graph access is available.
|
||||
- `channels.msteams.dangerouslyAllowNameMatching`: break-glass toggle to re-enable mutable UPN/display-name matching and direct team/channel name routing.
|
||||
- `channels.msteams.textChunkLimit`: outbound text chunk size.
|
||||
- `channels.msteams.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
|
||||
- `channels.msteams.mediaAllowHosts`: allowlist for inbound attachment hosts (defaults to Microsoft/Teams domains).
|
||||
- `channels.msteams.mediaAuthAllowHosts`: allowlist for attaching Authorization headers on media retries (defaults to Graph + Bot Framework hosts).
|
||||
- `channels.msteams.requireMention`: require @mention in channels/groups (default true).
|
||||
- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
|
||||
- `channels.msteams.teams.<teamId>.replyStyle`: per-team override.
|
||||
- `channels.msteams.teams.<teamId>.requireMention`: per-team override.
|
||||
- `channels.msteams.teams.<teamId>.tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing.
|
||||
- `channels.msteams.teams.<teamId>.toolsBySender`: default per-team per-sender tool policy overrides (`"*"` wildcard supported).
|
||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle`: per-channel override.
|
||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention`: per-channel override.
|
||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
|
||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported).
|
||||
- `toolsBySender` keys should use explicit prefixes:
|
||||
`id:`, `e164:`, `username:`, `name:` (legacy unprefixed keys still map to `id:` only).
|
||||
- `channels.msteams.actions.memberInfo`: enable or disable the Graph-backed member info action (default: enabled when Graph credentials are available).
|
||||
- `channels.msteams.authType`: authentication type — `"secret"` (default) or `"federated"`.
|
||||
- `channels.msteams.certificatePath`: path to PEM certificate file (federated + certificate auth).
|
||||
- `channels.msteams.certificateThumbprint`: certificate thumbprint (optional, not required for auth).
|
||||
- `channels.msteams.useManagedIdentity`: enable managed identity auth (federated mode).
|
||||
- `channels.msteams.managedIdentityClientId`: client ID for user-assigned managed identity.
|
||||
- `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)).
|
||||
|
||||
<Accordion title="Authentication">
|
||||
- `authType`: `"secret"` (default) or `"federated"`
|
||||
- `certificatePath`, `certificateThumbprint`: federated + certificate auth (thumbprint optional)
|
||||
- `useManagedIdentity`, `managedIdentityClientId`: federated + managed identity auth
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Access control">
|
||||
- `dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing)
|
||||
- `allowFrom`: DM allowlist, prefer AAD object IDs; the wizard resolves names when Graph access is available
|
||||
- `dangerouslyAllowNameMatching`: break-glass for mutable UPN/display-name and team/channel name routing
|
||||
- `requireMention`: require @mention in channels/groups (default `true`)
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Team and channel overrides">
|
||||
All of these override the top-level defaults:
|
||||
|
||||
- `teams.<teamId>.replyStyle`, `.requireMention`
|
||||
- `teams.<teamId>.tools`, `.toolsBySender`: per-team tool policy defaults
|
||||
- `teams.<teamId>.channels.<conversationId>.replyStyle`, `.requireMention`
|
||||
- `teams.<teamId>.channels.<conversationId>.tools`, `.toolsBySender`
|
||||
|
||||
`toolsBySender` keys accept `id:`, `e164:`, `username:`, `name:` prefixes (unprefixed keys map to `id:`). `"*"` is a wildcard.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Delivery, media, and actions">
|
||||
- `textChunkLimit`: outbound text chunk size
|
||||
- `chunkMode`: `length` (default) or `newline` (split on paragraph boundaries before length)
|
||||
- `mediaAllowHosts`: inbound attachment host allowlist (defaults to Microsoft/Teams domains)
|
||||
- `mediaAuthAllowHosts`: hosts that may receive Authorization headers on retries (defaults to Graph + Bot Framework)
|
||||
- `replyStyle`: `thread | top-level` (see [Reply style](#reply-style-threads-vs-posts))
|
||||
- `actions.memberInfo`: toggle the Graph-backed member info action (default on when Graph is available)
|
||||
- `sharePointSiteId`: required for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats))
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Routing and sessions
|
||||
## Routing & Sessions
|
||||
|
||||
- Session keys follow the standard agent format (see [/concepts/session](/concepts/session)):
|
||||
- Direct messages share the main session (`agent:<agentId>:<mainKey>`).
|
||||
@@ -606,7 +706,7 @@ Grouped settings (see `/gateway/configuration` for shared channel patterns).
|
||||
- `agent:<agentId>:msteams:channel:<conversationId>`
|
||||
- `agent:<agentId>:msteams:group:<conversationId>`
|
||||
|
||||
## Reply style: threads vs posts
|
||||
## Reply Style: Threads vs Posts
|
||||
|
||||
Teams recently introduced two channel UI styles over the same underlying data model:
|
||||
|
||||
@@ -641,7 +741,7 @@ Teams recently introduced two channel UI styles over the same underlying data mo
|
||||
}
|
||||
```
|
||||
|
||||
## Attachments and images
|
||||
## Attachments & Images
|
||||
|
||||
**Current limitations:**
|
||||
|
||||
@@ -724,7 +824,7 @@ Per-user sharing is more secure as only the chat participants can access the fil
|
||||
|
||||
Uploaded files are stored in a `/OpenClawShared/` folder in the configured SharePoint site's default document library.
|
||||
|
||||
## Polls (adaptive cards)
|
||||
## Polls (Adaptive Cards)
|
||||
|
||||
OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API).
|
||||
|
||||
@@ -733,7 +833,7 @@ OpenClaw sends Teams polls as Adaptive Cards (there is no native Teams poll API)
|
||||
- The gateway must stay online to record votes.
|
||||
- Polls do not auto-post result summaries yet (inspect the store file if needed).
|
||||
|
||||
## Presentation cards
|
||||
## Presentation Cards
|
||||
|
||||
Send semantic presentation payloads to Teams users or conversations using the `message` tool or CLI. OpenClaw renders them as Teams Adaptive Cards from the generic presentation contract.
|
||||
|
||||
@@ -821,7 +921,7 @@ Note: Without the `user:` prefix, names default to group/team resolution. Always
|
||||
- Proactive messages are only possible **after** a user has interacted, because we store conversation references at that point.
|
||||
- See `/gateway/configuration` for `dmPolicy` and allowlist gating.
|
||||
|
||||
## Team and channel IDs
|
||||
## Team and Channel IDs (Common Gotcha)
|
||||
|
||||
The `groupId` query parameter in Teams URLs is **NOT** the team ID used for configuration. Extract IDs from the URL path instead:
|
||||
|
||||
@@ -847,7 +947,7 @@ https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?gr
|
||||
- Channel ID = path segment after `/channel/` (URL-decoded)
|
||||
- **Ignore** the `groupId` query parameter
|
||||
|
||||
## Private channels
|
||||
## Private Channels
|
||||
|
||||
Bots have limited support in private channels:
|
||||
|
||||
@@ -897,23 +997,12 @@ Bots have limited support in private channels:
|
||||
- [RSC permissions reference](https://learn.microsoft.com/en-us/microsoftteams/platform/graph-api/rsc/resource-specific-consent)
|
||||
- [Teams bot file handling](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/bots-filesv4) (channel/group requires Graph)
|
||||
- [Proactive messaging](https://learn.microsoft.com/en-us/microsoftteams/platform/bots/how-to/conversations/send-proactive-messages)
|
||||
- [@microsoft/teams.cli](https://www.npmjs.com/package/@microsoft/teams.cli) - Teams CLI for bot management
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Channels overview" icon="list" href="/channels">
|
||||
All supported channels.
|
||||
</Card>
|
||||
<Card title="Pairing" icon="link" href="/channels/pairing">
|
||||
DM authentication and pairing flow.
|
||||
</Card>
|
||||
<Card title="Groups" icon="users" href="/channels/groups">
|
||||
Group chat behavior and mention gating.
|
||||
</Card>
|
||||
<Card title="Channel routing" icon="route" href="/channels/channel-routing">
|
||||
Session routing for messages.
|
||||
</Card>
|
||||
<Card title="Security" icon="shield" href="/gateway/security">
|
||||
Access model and hardening.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
- [Channels Overview](/channels) — all supported channels
|
||||
- [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
|
||||
- [Security](/gateway/security) — access model and hardening
|
||||
|
||||
@@ -546,6 +546,9 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
- default: audio file behavior
|
||||
- tag `[[audio_as_voice]]` in agent reply to force voice-note send
|
||||
- inbound voice-note transcripts are framed as machine-generated,
|
||||
untrusted text in the agent context; mention detection still uses the raw
|
||||
transcript so mention-gated voice messages continue to work.
|
||||
|
||||
Message action example:
|
||||
|
||||
|
||||
@@ -361,11 +361,12 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
|
||||
|
||||
<Accordion title="Outbound media behavior">
|
||||
- supports image, video, audio (PTT voice-note), and document payloads
|
||||
- reply payloads preserve `audioAsVoice`; WhatsApp sends audio media as Baileys PTT voice notes
|
||||
- non-Ogg audio, including Microsoft Edge TTS MP3/WebM output, is transcoded to Ogg/Opus before PTT delivery
|
||||
- native Ogg/Opus audio is sent with `audio/ogg; codecs=opus` for voice-note compatibility
|
||||
- audio media is sent through the Baileys `audio` payload with `ptt: true`, so WhatsApp clients render it as a push-to-talk voice note
|
||||
- reply payloads preserve `audioAsVoice`; TTS voice-note output for WhatsApp stays on this PTT path even when the provider returns MP3 or WebM
|
||||
- native Ogg/Opus audio is sent as `audio/ogg; codecs=opus` for voice-note compatibility
|
||||
- non-Ogg audio, including Microsoft Edge TTS MP3/WebM output, is transcoded with `ffmpeg` to 48 kHz mono Ogg/Opus before PTT delivery
|
||||
- animated GIF playback is supported via `gifPlayback: true` on video sends
|
||||
- captions are applied to the first media item when sending multi-media reply payloads
|
||||
- captions are applied to the first media item when sending multi-media reply payloads, except PTT voice notes send the audio first and visible text separately because WhatsApp clients do not render voice-note captions consistently
|
||||
- media source can be HTTP(S), `file://`, or local paths
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ The CI runs on every push to `main` and every pull request. It uses smart scopin
|
||||
|
||||
QA Lab has dedicated CI lanes outside the main smart-scoped workflow. The
|
||||
`Parity gate` workflow runs on matching PR changes and manual dispatch; it
|
||||
builds the private QA runtime and compares the mock GPT-5.4 and Opus 4.6
|
||||
builds the private QA runtime and compares the mock GPT-5.5 and Opus 4.6
|
||||
agentic packs. The `QA-Lab - All Lanes` workflow runs nightly on `main` and on
|
||||
manual dispatch; it fans out the mock parity gate, live Matrix lane, and live
|
||||
Telegram lane as parallel jobs. The live jobs use the `qa-live-shared`
|
||||
@@ -98,7 +98,7 @@ Local changed-lane logic lives in `scripts/changed-lanes.mjs` and is executed by
|
||||
|
||||
On pushes, the `checks` matrix adds the push-only `compat-node22` lane. On pull requests, that lane is skipped and the matrix stays focused on the normal test/channel lanes.
|
||||
|
||||
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, bundled plugin tests balance across six extension workers, small core unit lanes are paired, auto-reply runs as three balanced workers instead of six tiny workers, and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The broad agents lane uses the shared Vitest file-parallel scheduler because it is import/scheduling dominated rather than owned by a single slow test file. `runtime-config` runs with the infra core-runtime shard to keep the shared runtime shard from owning the tail. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built, keeping their old check names as lightweight verifier jobs while avoiding two extra Blacksmith workers and a second artifact-consumer queue.
|
||||
The slowest Node test families are split or balanced so each job stays small without over-reserving runners: channel contracts run as three weighted shards, bundled plugin tests balance across six extension workers, small core unit lanes are paired, auto-reply runs as four balanced workers with the reply subtree split into agent-runner, dispatch, and commands/state-routing shards, and agentic gateway/plugin configs are spread across the existing source-only agentic Node jobs instead of waiting on built artifacts. Broad browser, QA, media, and miscellaneous plugin tests use their dedicated Vitest configs instead of the shared plugin catch-all. Extension shard jobs run up to two plugin config groups at a time with one Vitest worker per group and a larger Node heap so import-heavy plugin batches do not create extra CI jobs. The broad agents lane uses the shared Vitest file-parallel scheduler because it is import/scheduling dominated rather than owned by a single slow test file. `runtime-config` runs with the infra core-runtime shard to keep the shared runtime shard from owning the tail. Include-pattern shards record timing entries using the CI shard name, so `.artifacts/vitest-shard-timings.json` can distinguish a whole config from a filtered shard. `check-additional` keeps package-boundary compile/canary work together and separates runtime topology architecture from gateway watch coverage; the boundary guard shard runs its small independent guards concurrently inside one job. Gateway watch, channel tests, and the core support-boundary shard run concurrently inside `build-artifacts` after `dist/` and `dist-runtime/` are already built, keeping their old check names as lightweight verifier jobs while avoiding two extra Blacksmith workers and a second artifact-consumer queue.
|
||||
Android CI runs both `testPlayDebugUnitTest` and `testThirdPartyDebugUnitTest`, then builds the Play debug APK. The third-party flavor has no separate source set or manifest; its unit-test lane still compiles that flavor with the SMS/call-log BuildConfig flags, while avoiding a duplicate debug APK packaging job on every Android-relevant push.
|
||||
`extension-fast` is PR-only because push runs already execute the full bundled plugin shards. That keeps changed-plugin feedback for reviews without reserving an extra Blacksmith worker on `main` for coverage already present in `checks-node-extensions`.
|
||||
|
||||
@@ -132,7 +132,10 @@ pnpm test:channels
|
||||
pnpm test:contracts:channels
|
||||
pnpm check:docs # docs format + lint + broken links
|
||||
pnpm build # build dist when CI artifact/build-smoke lanes matter
|
||||
pnpm ci:timings # summarize the latest origin/main push CI run
|
||||
pnpm ci:timings:recent # compare recent successful main CI runs
|
||||
node scripts/ci-run-timings.mjs <run-id> # summarize wall time, queue time, and slowest jobs
|
||||
node scripts/ci-run-timings.mjs --latest-main # ignore issue/comment noise and choose origin/main push CI
|
||||
node scripts/ci-run-timings.mjs --recent 10 # compare recent successful main CI runs
|
||||
pnpm test:perf:groups --full-suite --allow-failures --output .artifacts/test-perf/baseline-before.json
|
||||
pnpm test:perf:groups:compare .artifacts/test-perf/baseline-before.json .artifacts/test-perf/after-agent.json
|
||||
|
||||
@@ -138,6 +138,10 @@ the optional label, and the raw `targetId`. Agents should pass
|
||||
`suggestedTargetId` back into `focus`, `close`, snapshots, and actions. You can
|
||||
assign a label with `open --label`, `tab new --label`, or `tab label`; labels,
|
||||
tab ids, raw target ids, and unique target-id prefixes are all accepted.
|
||||
When Chromium replaces the underlying raw target during a navigation or form
|
||||
submit, OpenClaw keeps the stable `tabId`/label attached to the replacement tab
|
||||
when it can prove the match. Raw target ids remain volatile; prefer
|
||||
`suggestedTargetId`.
|
||||
|
||||
## Snapshot / screenshot / actions
|
||||
|
||||
@@ -185,6 +189,10 @@ openclaw browser wait --text "Done"
|
||||
openclaw browser evaluate --fn '(el) => el.textContent' --ref <ref>
|
||||
```
|
||||
|
||||
Action responses return the current raw `targetId` after action-triggered page
|
||||
replacement when OpenClaw can prove the replacement tab. Scripts should still
|
||||
store and pass `suggestedTargetId`/labels for long-lived workflows.
|
||||
|
||||
File + dialog helpers:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -239,13 +239,13 @@ Security contract for remote rescue:
|
||||
operation, where the runtime already has unsandboxed local authority.
|
||||
- Require an explicit owner identity. Rescue must not accept wildcard sender
|
||||
rules, open group policy, unauthenticated webhooks, or anonymous channels.
|
||||
- Owner DMs only by default. Group/channel rescue requires explicit opt-in and
|
||||
should still route approval prompts to the owner DM.
|
||||
- Owner DMs only by default. Group/channel rescue requires explicit opt-in.
|
||||
- Remote rescue cannot open the local TUI or switch into an interactive agent
|
||||
session. Use local `openclaw` for agent handoff.
|
||||
- Persistent writes still require approval, even in rescue mode.
|
||||
- Audit every applied rescue operation, including channel, account, sender,
|
||||
session key, operation, config hash before, and config hash after.
|
||||
- Audit every applied rescue operation. Message-channel rescue records channel,
|
||||
account, sender, and source-address metadata. Config-mutating operations also
|
||||
record config hashes before and after.
|
||||
- Never echo secrets. SecretRef inspection should report availability, not
|
||||
values.
|
||||
- If the Gateway is alive, prefer Gateway typed operations. If the Gateway is
|
||||
|
||||
@@ -138,6 +138,10 @@ Delivery ownership note:
|
||||
- `announce` fallback-delivers the final reply only when the agent did not send
|
||||
directly to the resolved target. `webhook` posts the finished payload to a URL.
|
||||
`none` disables runner fallback delivery.
|
||||
- Reminders created from an active chat preserve the live chat delivery target
|
||||
for fallback announce delivery. Internal session keys may be lowercase; do not
|
||||
use them as a source of truth for case-sensitive provider IDs such as Matrix
|
||||
room IDs.
|
||||
|
||||
## Common admin commands
|
||||
|
||||
|
||||
@@ -188,6 +188,9 @@ Notes:
|
||||
|
||||
- `gateway status` stays available for diagnostics even when the local CLI config is missing or invalid.
|
||||
- Default `gateway status` proves service state, WebSocket connect, and the auth capability visible at handshake time. It does not prove read/write/admin operations.
|
||||
- Diagnostic probes are non-mutating for first-time device auth: they reuse an
|
||||
existing cached device token when one exists, but they do not create a new CLI
|
||||
device identity or read-only device pairing record just to check status.
|
||||
- `gateway status` resolves configured auth SecretRefs for probe auth when possible.
|
||||
- If a required auth SecretRef is unresolved in this command path, `gateway status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
|
||||
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
|
||||
@@ -225,6 +228,8 @@ Interpretation:
|
||||
- `Capability: read-only|write-capable|admin-capable|pairing-pending|connect-only` reports what the probe could prove about auth. It is separate from reachability.
|
||||
- `Read probe: ok` means read-scope detail RPC calls (`health`/`status`/`system-presence`/`config.get`) also succeeded.
|
||||
- `Read probe: limited - missing scope: operator.read` means connect succeeded but read-scope RPC is limited. This is reported as **degraded** reachability, not full failure.
|
||||
- Like `gateway status`, probe reuses existing cached device auth but does not
|
||||
create first-time device identity or pairing state.
|
||||
- Exit code is non-zero only when no probed target is reachable.
|
||||
|
||||
JSON notes (`--json`):
|
||||
|
||||
@@ -209,7 +209,8 @@ deprecation warning and forwards to `openclaw plugins install`.
|
||||
|
||||
Npm specs are **registry-only** (package name + optional **exact version** or
|
||||
**dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency
|
||||
installs run with `--ignore-scripts` for safety.
|
||||
installs run project-local with `--ignore-scripts` for safety, even when your
|
||||
shell has global npm install settings.
|
||||
|
||||
Bare specs and `@latest` stay on the stable track. If npm resolves either of
|
||||
those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a
|
||||
|
||||
@@ -156,6 +156,9 @@ Use `image` for generation, edit, and description.
|
||||
```bash
|
||||
openclaw infer image generate --prompt "friendly lobster illustration" --json
|
||||
openclaw infer image generate --prompt "cinematic product photo of headphones" --json
|
||||
openclaw infer image generate --model openai/gpt-image-1.5 --output-format png --background transparent --prompt "simple red circle sticker on a transparent background" --json
|
||||
openclaw infer image generate --prompt "slow image backend" --timeout-ms 180000 --json
|
||||
openclaw infer image edit --file ./logo.png --model openai/gpt-image-1.5 --output-format png --background transparent --prompt "keep the logo, remove the background" --json
|
||||
openclaw infer image describe --file ./photo.jpg --json
|
||||
openclaw infer image describe --file ./ui-screenshot.png --model openai/gpt-4.1-mini --json
|
||||
openclaw infer image describe --file ./photo.jpg --model ollama/qwen2.5vl:7b --json
|
||||
@@ -164,6 +167,10 @@ openclaw infer image describe --file ./photo.jpg --model ollama/qwen2.5vl:7b --j
|
||||
Notes:
|
||||
|
||||
- Use `image edit` when starting from existing input files.
|
||||
- Use `--output-format png --background transparent` with
|
||||
`--model openai/gpt-image-1.5` for transparent-background OpenAI PNG output;
|
||||
`--openai-background` remains available as an OpenAI-specific alias. Providers
|
||||
that do not declare background support report the hint as an ignored override.
|
||||
- Use `image providers --json` to verify which bundled image providers are
|
||||
discoverable, configured, selected, and which generation/edit capabilities
|
||||
each provider exposes.
|
||||
|
||||
@@ -384,6 +384,11 @@ Important behavior:
|
||||
milliseconds of idle time (default 10 minutes; set `0` to disable) and
|
||||
one-shot embedded runs clean them up at run end
|
||||
|
||||
Runtime adapters may normalize this shared registry into the shape their
|
||||
downstream client expects. For example, embedded Pi consumes OpenClaw
|
||||
`transport` values directly, while Claude Code and Gemini receive CLI-native
|
||||
`type` values such as `http`, `sse`, or `stdio`.
|
||||
|
||||
## Saved MCP server definitions
|
||||
|
||||
OpenClaw also stores a lightweight MCP server registry in config for surfaces
|
||||
|
||||
@@ -31,6 +31,8 @@ openclaw plugins inspect --all
|
||||
openclaw plugins info <id>
|
||||
openclaw plugins enable <id>
|
||||
openclaw plugins disable <id>
|
||||
openclaw plugins registry
|
||||
openclaw plugins registry --refresh
|
||||
openclaw plugins uninstall <id>
|
||||
openclaw plugins doctor
|
||||
openclaw plugins update <id-or-npm-spec>
|
||||
@@ -107,7 +109,8 @@ visibility and per-hook enablement, not package installation.
|
||||
|
||||
Npm specs are **registry-only** (package name + optional **exact version** or
|
||||
**dist-tag**). Git/URL/file specs and semver ranges are rejected. Dependency
|
||||
installs run with `--ignore-scripts` for safety.
|
||||
installs run project-local with `--ignore-scripts` for safety, even when your
|
||||
shell has global npm install settings.
|
||||
|
||||
Bare specs and `@latest` stay on the stable track. If npm resolves either of
|
||||
those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a
|
||||
@@ -195,18 +198,20 @@ openclaw plugins list --verbose
|
||||
openclaw plugins list --json
|
||||
```
|
||||
|
||||
Use `--enabled` to show only loaded plugins. Use `--verbose` to switch from the
|
||||
Use `--enabled` to show only enabled plugins. Use `--verbose` to switch from the
|
||||
table view to per-plugin detail lines with source/origin/version/activation
|
||||
metadata. Use `--json` for machine-readable inventory plus registry
|
||||
diagnostics.
|
||||
|
||||
`plugins list` runs discovery from the current CLI environment and config. It is
|
||||
useful for checking whether a plugin is enabled/loadable, but it is not a live
|
||||
runtime probe of an already-running Gateway process. After changing plugin code,
|
||||
enablement, hook policy, or `plugins.load.paths`, restart the Gateway that
|
||||
serves the channel before expecting new `register(api)` code or hooks to run.
|
||||
For remote/container deployments, verify you are restarting the actual
|
||||
`openclaw gateway run` child, not only a wrapper process.
|
||||
`plugins list` reads the persisted local plugin registry first, with a
|
||||
manifest-only derived fallback when the registry is missing or invalid. It is
|
||||
useful for checking whether a plugin is installed, enabled, and visible to cold
|
||||
startup planning, but it is not a live runtime probe of an already-running
|
||||
Gateway process. After changing plugin code, enablement, hook policy, or
|
||||
`plugins.load.paths`, restart the Gateway that serves the channel before
|
||||
expecting new `register(api)` code or hooks to run. For remote/container
|
||||
deployments, verify you are restarting the actual `openclaw gateway run` child,
|
||||
not only a wrapper process.
|
||||
|
||||
For runtime hook debugging:
|
||||
|
||||
@@ -214,7 +219,8 @@ For runtime hook debugging:
|
||||
from a module-loaded inspection pass.
|
||||
- `openclaw gateway status --deep --require-rpc` confirms the reachable Gateway,
|
||||
service/process hints, config path, and RPC health.
|
||||
- Non-bundled conversation hooks (`llm_input`, `llm_output`, `agent_end`) require
|
||||
- Non-bundled conversation hooks (`llm_input`, `llm_output`,
|
||||
`before_agent_finalize`, `agent_end`) require
|
||||
`plugins.entries.<id>.hooks.allowConversationAccess=true`.
|
||||
|
||||
Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`):
|
||||
@@ -227,7 +233,17 @@ openclaw plugins install -l ./my-plugin
|
||||
source path instead of copying over a managed install target.
|
||||
|
||||
Use `--pin` on npm installs to save the resolved exact spec (`name@version`) in
|
||||
`plugins.installs` while keeping the default behavior unpinned.
|
||||
the managed plugin index while keeping the default behavior unpinned.
|
||||
|
||||
### Plugin Index
|
||||
|
||||
Plugin install metadata is machine-managed state, not user config. Installs
|
||||
and updates write it to `plugins/installs.json` under the active OpenClaw state
|
||||
directory. Its top-level `installRecords` map is the durable source of install
|
||||
metadata, including records for broken or missing plugin manifests. The
|
||||
`plugins` array is the manifest-derived cold registry cache. The file includes a
|
||||
do-not-edit warning and is used by `openclaw plugins update`, uninstall,
|
||||
diagnostics, and the cold plugin registry.
|
||||
|
||||
### Uninstall
|
||||
|
||||
@@ -237,8 +253,9 @@ openclaw plugins uninstall <id> --dry-run
|
||||
openclaw plugins uninstall <id> --keep-files
|
||||
```
|
||||
|
||||
`uninstall` removes plugin records from `plugins.entries`, `plugins.installs`,
|
||||
the plugin allowlist, and linked `plugins.load.paths` entries when applicable.
|
||||
`uninstall` removes plugin records from `plugins.entries`, the persisted plugin
|
||||
index, the plugin allowlist, and linked `plugins.load.paths` entries when
|
||||
applicable.
|
||||
For active memory plugins, the memory slot resets to `memory-core`.
|
||||
|
||||
By default, uninstall also removes the plugin install directory under the active
|
||||
@@ -257,8 +274,8 @@ openclaw plugins update @openclaw/voice-call@beta
|
||||
openclaw plugins update openclaw-codex-app-server --dangerously-force-unsafe-install
|
||||
```
|
||||
|
||||
Updates apply to tracked installs in `plugins.installs` and tracked hook-pack
|
||||
installs in `hooks.internal.installs`.
|
||||
Updates apply to tracked plugin installs in the managed plugin index and
|
||||
tracked hook-pack installs in `hooks.internal.installs`.
|
||||
|
||||
When you pass a plugin id, OpenClaw reuses the recorded install spec for that
|
||||
plugin. That means previously stored dist-tags such as `@beta` and exact pinned
|
||||
@@ -333,6 +350,29 @@ For module-shape failures such as missing `register`/`activate` exports, rerun
|
||||
with `OPENCLAW_PLUGIN_LOAD_DEBUG=1` to include a compact export-shape summary in
|
||||
the diagnostic output.
|
||||
|
||||
### Registry
|
||||
|
||||
```bash
|
||||
openclaw plugins registry
|
||||
openclaw plugins registry --refresh
|
||||
openclaw plugins registry --json
|
||||
```
|
||||
|
||||
The local plugin registry is OpenClaw's persisted cold read model for installed
|
||||
plugin identity, enablement, source metadata, and contribution ownership.
|
||||
Normal startup, provider owner lookup, channel setup classification, and plugin
|
||||
inventory can read it without importing plugin runtime modules.
|
||||
|
||||
Use `plugins registry` to inspect whether the persisted registry is present,
|
||||
current, or stale. Use `--refresh` to rebuild it from the persisted plugin
|
||||
index, config policy, and manifest/package metadata. This is a repair path, not
|
||||
a runtime activation path.
|
||||
|
||||
`OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY=1` is a deprecated break-glass
|
||||
compatibility switch for registry read failures. Prefer `plugins registry
|
||||
--refresh` or `openclaw doctor --fix`; the env fallback is only for emergency
|
||||
startup recovery while the migration rolls out.
|
||||
|
||||
### Marketplace
|
||||
|
||||
```bash
|
||||
|
||||
@@ -46,6 +46,20 @@ That means OpenClaw selects an OpenAI model ref, then asks the Codex app-server
|
||||
runtime to run the embedded agent turn. It does not mean the channel, model
|
||||
provider catalog, or OpenClaw session store becomes Codex.
|
||||
|
||||
When the bundled `codex` plugin is enabled, natural-language Codex control
|
||||
should use the native `/codex` command surface (`/codex bind`, `/codex threads`,
|
||||
`/codex resume`, `/codex steer`, `/codex stop`) instead of ACP. Use ACP for
|
||||
Codex only when the user explicitly asks for ACP/acpx or is testing the ACP
|
||||
adapter path. Claude Code, Gemini CLI, OpenCode, Cursor, and similar external
|
||||
harnesses still use ACP.
|
||||
|
||||
| You mean... | Use... |
|
||||
| --------------------------------------- | -------------------------------------------- |
|
||||
| Codex app-server chat/thread control | `/codex ...` from the bundled `codex` plugin |
|
||||
| Codex app-server embedded agent runtime | `embeddedHarness.runtime: "codex"` |
|
||||
| OpenAI Codex OAuth on the PI runner | `openai-codex/*` model refs |
|
||||
| Claude Code or other external harness | ACP/acpx |
|
||||
|
||||
For the OpenAI-family prefix split, see [OpenAI](/providers/openai) and
|
||||
[Model providers](/concepts/model-providers). For the Codex runtime support
|
||||
contract, see [Codex harness](/plugins/codex-harness#v1-support-contract).
|
||||
|
||||
@@ -135,6 +135,11 @@ earlier conversations. This is opt-in via
|
||||
**Only keyword matches?** Your embedding provider may not be configured. Check
|
||||
`openclaw memory status --deep`.
|
||||
|
||||
**Local embeddings time out?** `ollama`, `lmstudio`, and `local` use a longer
|
||||
inline batch timeout by default. If the host is simply slow, set
|
||||
`agents.defaults.memorySearch.sync.embeddingBatchTimeoutSeconds` and rerun
|
||||
`openclaw memory index --force`.
|
||||
|
||||
**CJK text not found?** Rebuild the FTS index with
|
||||
`openclaw memory index --force`.
|
||||
|
||||
|
||||
@@ -77,6 +77,19 @@ gateway-backed session transcript, so they are the source of truth.
|
||||
|
||||
Details: [Session management](/concepts/session).
|
||||
|
||||
## Tool result metadata
|
||||
|
||||
Tool result `content` is the model-visible result. Tool result `details` is
|
||||
runtime metadata for UI rendering, diagnostics, media delivery, and plugins.
|
||||
|
||||
OpenClaw keeps that boundary explicit:
|
||||
|
||||
- `toolResult.details` is stripped before provider replay and compaction input.
|
||||
- Persisted session transcripts keep only bounded `details`; oversized metadata
|
||||
is replaced with a compact summary marked `persistedDetailsTruncated: true`.
|
||||
- Plugins and tools should put text the model must read in `content`, not only
|
||||
in `details`.
|
||||
|
||||
## Inbound bodies and history context
|
||||
|
||||
OpenClaw separates the **prompt body** from the **command body**:
|
||||
@@ -154,6 +167,8 @@ Details: [Configuration](/gateway/config-agents#messages) and channel docs.
|
||||
## Silent replies
|
||||
|
||||
The exact silent token `NO_REPLY` / `no_reply` means “do not deliver a user-visible reply”.
|
||||
When a turn also has pending tool media, such as generated TTS audio, OpenClaw
|
||||
strips the silent text but still delivers the media attachment.
|
||||
OpenClaw resolves that behavior by conversation type:
|
||||
|
||||
- Direct conversations disallow silence by default and rewrite a bare silent
|
||||
|
||||
@@ -129,15 +129,18 @@ validation failures) are treated as failover‑worthy and use the same cooldowns
|
||||
OpenAI-compatible stop-reason errors such as `Unhandled stop reason: error`,
|
||||
`stop reason: error`, and `reason: error` are classified as timeout/failover
|
||||
signals.
|
||||
Provider-scoped generic server text can also land in that timeout bucket when
|
||||
the source matches a known transient pattern. For example, Anthropic bare
|
||||
`An unknown error occurred` and JSON `api_error` payloads with transient server
|
||||
text such as `internal server error`, `unknown error, 520`, `upstream error`,
|
||||
or `backend error` are treated as failover-worthy timeouts. OpenRouter-specific
|
||||
generic upstream text such as bare `Provider returned error` is also treated as
|
||||
timeout only when the provider context is actually OpenRouter. Generic internal
|
||||
fallback text such as `LLM request failed with an unknown error.` stays
|
||||
conservative and does not trigger failover by itself.
|
||||
Generic server text can also land in that timeout bucket when the source matches
|
||||
a known transient pattern. For example, the bare pi-ai stream-wrapper message
|
||||
`An unknown error occurred` is treated as failover-worthy for every provider
|
||||
because pi-ai emits it when provider streams end with `stopReason: "aborted"` or
|
||||
`stopReason: "error"` without specific details. JSON `api_error` payloads with
|
||||
transient server text such as `internal server error`, `unknown error, 520`,
|
||||
`upstream error`, or `backend error` are also treated as failover-worthy
|
||||
timeouts.
|
||||
OpenRouter-specific generic upstream text such as bare `Provider returned error`
|
||||
is treated as timeout only when the provider context is actually OpenRouter.
|
||||
Generic internal fallback text such as `LLM request failed with an unknown
|
||||
error.` stays conservative and does not trigger failover by itself.
|
||||
|
||||
Some provider SDKs may otherwise sleep for a long `Retry-After` window before
|
||||
returning control to OpenClaw. For Stainless-based SDKs such as Anthropic and
|
||||
|
||||
@@ -30,9 +30,9 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram)
|
||||
`google-gemini-cli`, or `codex-cli` when you want a local CLI backend.
|
||||
Legacy `claude-cli/*`, `google-gemini-cli/*`, and `codex-cli/*` refs migrate
|
||||
back to canonical provider refs with the runtime recorded separately.
|
||||
- GPT-5.5 is available through `openai-codex/gpt-5.5` in PI, the native
|
||||
Codex app-server harness, and the public OpenAI API when the bundled PI
|
||||
catalog exposes `openai/gpt-5.5` for your install.
|
||||
- GPT-5.5 is available through `openai/gpt-5.5` for direct API-key traffic,
|
||||
`openai-codex/gpt-5.5` in PI for Codex OAuth, and the native Codex
|
||||
app-server harness when `embeddedHarness.runtime: "codex"` is set.
|
||||
|
||||
## Plugin-owned provider behavior
|
||||
|
||||
@@ -71,10 +71,9 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
||||
- Provider: `openai`
|
||||
- Auth: `OPENAI_API_KEY`
|
||||
- Optional rotation: `OPENAI_API_KEYS`, `OPENAI_API_KEY_1`, `OPENAI_API_KEY_2`, plus `OPENCLAW_LIVE_OPENAI_KEY` (single override)
|
||||
- Example models: `openai/gpt-5.5`, `openai/gpt-5.4`, `openai/gpt-5.4-mini`
|
||||
- GPT-5.5 direct API support depends on the bundled PI catalog version for
|
||||
your install; verify with `openclaw models list --provider openai` before
|
||||
using `openai/gpt-5.5` without the Codex app-server runtime.
|
||||
- Example models: `openai/gpt-5.5`, `openai/gpt-5.4-mini`
|
||||
- Verify account/model availability with `openclaw models list --provider openai`
|
||||
if a specific install or API key behaves differently.
|
||||
- CLI: `openclaw onboard --auth-choice openai-api-key`
|
||||
- Default transport is `auto` (WebSocket-first, SSE fallback)
|
||||
- Override per model via `agents.defaults.models["openai/<model>"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`)
|
||||
@@ -91,7 +90,7 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.4" } } },
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.5" } } },
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -50,6 +50,21 @@ pnpm qa:lab:watch
|
||||
rebuilds that bundle on change, and the browser auto-reloads when the QA Lab
|
||||
asset hash changes.
|
||||
|
||||
For a local OpenTelemetry trace smoke, run:
|
||||
|
||||
```bash
|
||||
pnpm qa:otel:smoke
|
||||
```
|
||||
|
||||
That script starts a local OTLP/HTTP trace receiver, runs the
|
||||
`otel-trace-smoke` QA scenario with the `diagnostics-otel` plugin enabled, then
|
||||
decodes the exported protobuf spans and asserts the release-critical shape:
|
||||
`openclaw.run`, `openclaw.model.call`, `openclaw.context.assembled`, and
|
||||
`openclaw.message.delivery` must be present; model calls must not export
|
||||
`StreamAbandoned` on successful turns; raw diagnostic IDs and
|
||||
`openclaw.content.*` attributes must stay out of the trace. It writes
|
||||
`otel-smoke-summary.json` next to the QA suite artifacts.
|
||||
|
||||
For a transport-real Matrix smoke lane, run:
|
||||
|
||||
```bash
|
||||
@@ -238,7 +253,7 @@ refs and write a judged Markdown report:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa character-eval \
|
||||
--model openai/gpt-5.4,thinking=medium,fast \
|
||||
--model openai/gpt-5.5,thinking=medium,fast \
|
||||
--model openai/gpt-5.2,thinking=xhigh \
|
||||
--model openai/gpt-5,thinking=xhigh \
|
||||
--model anthropic/claude-opus-4-6,thinking=high \
|
||||
@@ -246,7 +261,7 @@ pnpm openclaw qa character-eval \
|
||||
--model zai/glm-5.1,thinking=high \
|
||||
--model moonshot/kimi-k2.5,thinking=high \
|
||||
--model google/gemini-3.1-pro-preview,thinking=high \
|
||||
--judge-model openai/gpt-5.4,thinking=xhigh,fast \
|
||||
--judge-model openai/gpt-5.5,thinking=xhigh,fast \
|
||||
--judge-model anthropic/claude-opus-4-6,thinking=high \
|
||||
--blind-judge-models \
|
||||
--concurrency 16 \
|
||||
@@ -263,7 +278,7 @@ Use `--blind-judge-models` when comparing providers: the judge prompt still gets
|
||||
every transcript and run status, but candidate refs are replaced with neutral
|
||||
labels such as `candidate-01`; the report maps rankings back to real refs after
|
||||
parsing.
|
||||
Candidate runs default to `high` thinking, with `medium` for GPT-5.4 and `xhigh`
|
||||
Candidate runs default to `high` thinking, with `medium` for GPT-5.5 and `xhigh`
|
||||
for older OpenAI eval refs that support it. Override a specific candidate inline with
|
||||
`--model provider/model,thinking=<level>`. `--thinking <level>` still sets a
|
||||
global fallback, and the older `--model-thinking <provider/model=level>` form is
|
||||
@@ -278,12 +293,12 @@ Candidate and judge model runs both default to concurrency 16. Lower
|
||||
`--concurrency` or `--judge-concurrency` when provider limits or local gateway
|
||||
pressure make a run too noisy.
|
||||
When no candidate `--model` is passed, the character eval defaults to
|
||||
`openai/gpt-5.4`, `openai/gpt-5.2`, `openai/gpt-5`, `anthropic/claude-opus-4-6`,
|
||||
`openai/gpt-5.5`, `openai/gpt-5.2`, `openai/gpt-5`, `anthropic/claude-opus-4-6`,
|
||||
`anthropic/claude-sonnet-4-6`, `zai/glm-5.1`,
|
||||
`moonshot/kimi-k2.5`, and
|
||||
`google/gemini-3.1-pro-preview` when no `--model` is passed.
|
||||
When no `--judge-model` is passed, the judges default to
|
||||
`openai/gpt-5.4,thinking=xhigh,fast` and
|
||||
`openai/gpt-5.5,thinking=xhigh,fast` and
|
||||
`anthropic/claude-opus-4-6,thinking=high`.
|
||||
|
||||
## Related docs
|
||||
|
||||
@@ -35,15 +35,23 @@ cache-write size, directly lowering cost.
|
||||
|
||||
## Legacy image cleanup
|
||||
|
||||
OpenClaw also runs a separate idempotent cleanup for older legacy sessions that
|
||||
persisted raw image blocks in history.
|
||||
OpenClaw also builds a separate idempotent replay view for sessions that
|
||||
persist raw image blocks or prompt-hydration media markers in history.
|
||||
|
||||
- It preserves the **3 most recent completed turns** byte-for-byte so prompt
|
||||
cache prefixes for recent follow-ups stay stable.
|
||||
- Older already-processed image blocks in `user` or `toolResult` history can be
|
||||
replaced with `[image data removed - already processed by model]`.
|
||||
- In the replay view, older already-processed image blocks from `user` or
|
||||
`toolResult` history can be replaced with
|
||||
`[image data removed - already processed by model]`.
|
||||
- Older textual media references such as `[media attached: ...]`,
|
||||
`[Image: source: ...]`, and `media://inbound/...` can be replaced with
|
||||
`[media reference removed - already processed by model]`. Current-turn
|
||||
attachment markers stay intact so vision models can still hydrate fresh
|
||||
images.
|
||||
- The raw session transcript is not rewritten, so history viewers can still
|
||||
render the original message entries and their images.
|
||||
- This is separate from normal cache-TTL pruning. It exists to stop repeated
|
||||
image payloads from busting prompt caches on later turns.
|
||||
image payloads or stale media refs from busting prompt caches on later turns.
|
||||
|
||||
## Smart defaults
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ summary: "How OpenClaw manages conversation sessions"
|
||||
read_when:
|
||||
- You want to understand session routing and isolation
|
||||
- You want to configure DM scope for multi-user setups
|
||||
- You are debugging daily or idle session resets
|
||||
title: "Session management"
|
||||
---
|
||||
|
||||
@@ -59,13 +60,21 @@ Verify your setup with `openclaw security audit`.
|
||||
Sessions are reused until they expire:
|
||||
|
||||
- **Daily reset** (default) -- new session at 4:00 AM local time on the gateway
|
||||
host.
|
||||
host. Daily freshness is based on when the current `sessionId` started, not
|
||||
on later metadata writes.
|
||||
- **Idle reset** (optional) -- new session after a period of inactivity. Set
|
||||
`session.reset.idleMinutes`.
|
||||
`session.reset.idleMinutes`. Idle freshness is based on the last real
|
||||
user/channel interaction, so heartbeat, cron, and exec system events do not
|
||||
keep the session alive.
|
||||
- **Manual reset** -- type `/new` or `/reset` in chat. `/new <model>` also
|
||||
switches the model.
|
||||
|
||||
When both daily and idle resets are configured, whichever expires first wins.
|
||||
Heartbeat, cron, exec, and other system-event turns may write session metadata,
|
||||
but those writes do not extend daily or idle reset freshness. When a reset
|
||||
rolls the session, queued system-event notices for the old session are
|
||||
discarded so stale background updates are not prepended to the first prompt in
|
||||
the new session.
|
||||
|
||||
Sessions with an active provider-owned CLI session are not cut by the implicit
|
||||
daily default. Use `/reset` or configure `session.reset` explicitly when those
|
||||
@@ -79,6 +88,18 @@ session data.
|
||||
- **Store:** `~/.openclaw/agents/<agentId>/sessions/sessions.json`
|
||||
- **Transcripts:** `~/.openclaw/agents/<agentId>/sessions/<sessionId>.jsonl`
|
||||
|
||||
`sessions.json` keeps separate lifecycle timestamps:
|
||||
|
||||
- `sessionStartedAt`: when the current `sessionId` began; daily reset uses this.
|
||||
- `lastInteractionAt`: last user/channel interaction that extends idle lifetime.
|
||||
- `updatedAt`: last store-row mutation; useful for listing and pruning, but not
|
||||
authoritative for daily/idle reset freshness.
|
||||
|
||||
Older rows without `sessionStartedAt` are resolved from the transcript JSONL
|
||||
session header when available. If an older row also lacks `lastInteractionAt`,
|
||||
idle freshness falls back to that session start time, not to later bookkeeping
|
||||
writes.
|
||||
|
||||
## Session maintenance
|
||||
|
||||
OpenClaw automatically bounds session storage over time. By default, it runs
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/help/gpt54-codex-agentic-parity",
|
||||
"destination": "/help/gpt55-codex-agentic-parity"
|
||||
},
|
||||
{
|
||||
"source": "/help/gpt54-codex-agentic-parity-maintainers",
|
||||
"destination": "/help/gpt55-codex-agentic-parity-maintainers"
|
||||
},
|
||||
{
|
||||
"source": "/mcp",
|
||||
"destination": "/cli/mcp"
|
||||
@@ -1293,6 +1301,7 @@
|
||||
"providers/bedrock-mantle",
|
||||
"providers/anthropic",
|
||||
"providers/arcee",
|
||||
"providers/azure-speech",
|
||||
"providers/chutes",
|
||||
"providers/claude-max-api-proxy",
|
||||
"providers/cloudflare-ai-gateway",
|
||||
@@ -1309,6 +1318,7 @@
|
||||
"providers/groq",
|
||||
"providers/huggingface",
|
||||
"providers/inferrs",
|
||||
"providers/inworld",
|
||||
"providers/kilocode",
|
||||
"providers/litellm",
|
||||
"providers/lmstudio",
|
||||
@@ -1427,11 +1437,12 @@
|
||||
"group": "Health and diagnostics",
|
||||
"pages": [
|
||||
"gateway/health",
|
||||
"gateway/diagnostics",
|
||||
"gateway/heartbeat",
|
||||
"gateway/doctor",
|
||||
"gateway/logging",
|
||||
"logging",
|
||||
"gateway/opentelemetry",
|
||||
"gateway/logging",
|
||||
"gateway/diagnostics",
|
||||
"gateway/troubleshooting"
|
||||
]
|
||||
},
|
||||
@@ -1649,8 +1660,8 @@
|
||||
"concepts/typing-indicators",
|
||||
"concepts/usage-tracking",
|
||||
"concepts/timezone",
|
||||
"help/gpt54-codex-agentic-parity",
|
||||
"help/gpt54-codex-agentic-parity-maintainers"
|
||||
"help/gpt55-codex-agentic-parity",
|
||||
"help/gpt55-codex-agentic-parity-maintainers"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -139,6 +139,7 @@ The Gateway writes a rolling log file (printed on startup as
|
||||
- `bonjour: advertise failed ...`
|
||||
- `bonjour: ... name conflict resolved` / `hostname conflict resolved`
|
||||
- `bonjour: watchdog detected non-announced service ...`
|
||||
- `bonjour: disabling advertiser after ... failed restarts ...`
|
||||
|
||||
## Debugging on iOS node
|
||||
|
||||
@@ -155,6 +156,10 @@ The log includes browser state transitions and result‑set changes.
|
||||
|
||||
- **Bonjour doesn’t cross networks**: use Tailnet or SSH.
|
||||
- **Multicast blocked**: some Wi‑Fi networks disable mDNS.
|
||||
- **Advertiser stuck in probing/announcing**: hosts with blocked multicast,
|
||||
container bridges, WSL, or interface churn can leave the ciao advertiser in a
|
||||
non-announced state. OpenClaw retries a few times and then disables Bonjour
|
||||
for the current Gateway process instead of restarting the advertiser forever.
|
||||
- **Sleep / interface churn**: macOS may temporarily drop mDNS results; retry.
|
||||
- **Browse works but resolve fails**: keep machine names simple (avoid emojis or
|
||||
punctuation), then restart the Gateway. The service instance name derives from
|
||||
|
||||
@@ -342,8 +342,8 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
- Also used as fallback routing when the selected/default model cannot accept image input.
|
||||
- `imageGenerationModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`).
|
||||
- Used by the shared image-generation capability and any future tool/plugin surface that generates images.
|
||||
- Typical values: `google/gemini-3.1-flash-image-preview` for native Gemini image generation, `fal/fal-ai/flux/dev` for fal, or `openai/gpt-image-2` for OpenAI Images.
|
||||
- If you select a provider/model directly, configure matching provider auth too (for example `GEMINI_API_KEY` or `GOOGLE_API_KEY` for `google/*`, `OPENAI_API_KEY` or OpenAI Codex OAuth for `openai/gpt-image-2`, `FAL_KEY` for `fal/*`).
|
||||
- Typical values: `google/gemini-3.1-flash-image-preview` for native Gemini image generation, `fal/fal-ai/flux/dev` for fal, `openai/gpt-image-2` for OpenAI Images, or `openai/gpt-image-1.5` for transparent-background OpenAI PNG/WebP output.
|
||||
- If you select a provider/model directly, configure matching provider auth too (for example `GEMINI_API_KEY` or `GOOGLE_API_KEY` for `google/*`, `OPENAI_API_KEY` or OpenAI Codex OAuth for `openai/gpt-image-2` / `openai/gpt-image-1.5`, `FAL_KEY` for `fal/*`).
|
||||
- If omitted, `image_generate` can still infer an auth-backed provider default. It tries the current default provider first, then the remaining registered image-generation providers in provider-id order.
|
||||
- `musicGenerationModel`: accepts either a string (`"provider/model"`) or an object (`{ primary, fallbacks }`).
|
||||
- Used by the shared music-generation capability and the built-in `music_generate` tool.
|
||||
@@ -363,7 +363,7 @@ Time format in system prompt. Default: `auto` (OS preference).
|
||||
- `pdfMaxPages`: default maximum pages considered by extraction fallback mode in the `pdf` tool.
|
||||
- `verboseDefault`: default verbose level for agents. Values: `"off"`, `"on"`, `"full"`. Default: `"off"`.
|
||||
- `elevatedDefault`: default elevated-output level for agents. Values: `"off"`, `"on"`, `"ask"`, `"full"`. Default: `"on"`.
|
||||
- `model.primary`: format `provider/model` (e.g. `openai/gpt-5.4` 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 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.
|
||||
- `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`, `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.
|
||||
@@ -406,16 +406,16 @@ Codex app-server harness. For the mental model, see
|
||||
|
||||
**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.4` or configured Codex OAuth 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` 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` |
|
||||
|
||||
Your configured aliases always win over defaults.
|
||||
|
||||
@@ -899,6 +899,14 @@ scripts/sandbox-browser-setup.sh # optional browser image
|
||||
|
||||
### `agents.list` (per-agent overrides)
|
||||
|
||||
Use `agents.list[].tts` to give an agent its own TTS provider, voice, model,
|
||||
style, or auto-TTS mode. The agent block deep-merges over global
|
||||
`messages.tts`, so shared credentials can stay in one place while individual
|
||||
agents override only the voice or provider fields they need. The active agent's
|
||||
override applies to automatic spoken replies, `/tts audio`, `/tts status`, and
|
||||
the `tts` agent tool. See [Text-to-speech](/tools/tts#per-agent-voice-overrides)
|
||||
for provider examples and precedence.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
@@ -915,6 +923,11 @@ scripts/sandbox-browser-setup.sh # optional browser image
|
||||
fastModeDefault: false, // per-agent fast mode override
|
||||
embeddedHarness: { runtime: "auto", fallback: "pi" },
|
||||
params: { cacheRetention: "none" }, // overrides matching defaults.models params by key
|
||||
tts: {
|
||||
providers: {
|
||||
elevenlabs: { voiceId: "EXAVITQu4vr4xnSDxMaL" },
|
||||
},
|
||||
},
|
||||
skills: ["docs-search"], // replaces agents.defaults.skills when set
|
||||
identity: {
|
||||
name: "Samantha",
|
||||
@@ -950,6 +963,7 @@ scripts/sandbox-browser-setup.sh # optional browser image
|
||||
- `default`: when multiple are set, first wins (warning logged). If none set, first list entry is default.
|
||||
- `model`: string form overrides `primary` only; object form `{ primary, fallbacks }` overrides both (`[]` disables global fallbacks). Cron jobs that only override `primary` still inherit default fallbacks unless you set `fallbacks: []`.
|
||||
- `params`: per-agent stream params merged over the selected model entry in `agents.defaults.models`. Use this for agent-specific overrides like `cacheRetention`, `temperature`, or `maxTokens` without duplicating the whole model catalog.
|
||||
- `tts`: optional per-agent text-to-speech overrides. The block deep-merges over `messages.tts`, so keep shared provider credentials and fallback policy in `messages.tts` and set only persona-specific values such as provider, voice, model, style, or auto mode here.
|
||||
- `skills`: optional per-agent skill allowlist. If omitted, the agent inherits `agents.defaults.skills` when set; an explicit list replaces defaults instead of merging, and `[]` means no skills.
|
||||
- `thinkingDefault`: optional per-agent default thinking level (`off | minimal | low | medium | high | xhigh | adaptive | max`). Overrides `agents.defaults.thinkingDefault` for this agent when no per-message or session override is set. The selected provider/model profile controls which values are valid; for Google Gemini, `adaptive` keeps provider-owned dynamic thinking (`thinkingLevel` omitted on Gemini 3/3.1, `thinkingBudget: -1` on Gemini 2.5).
|
||||
- `reasoningDefault`: optional per-agent default reasoning visibility (`on | off | stream`). Applies when no per-message or session reasoning override is set.
|
||||
@@ -1162,7 +1176,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
- `per-channel-peer`: isolate per channel + sender (recommended for multi-user inboxes).
|
||||
- `per-account-channel-peer`: isolate per account + channel + sender (recommended for multi-account).
|
||||
- **`identityLinks`**: map canonical ids to provider-prefixed peers for cross-channel session sharing.
|
||||
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins.
|
||||
- **`reset`**: primary reset policy. `daily` resets at `atHour` local time; `idle` resets after `idleMinutes`. When both configured, whichever expires first wins. Daily reset freshness uses the session row's `sessionStartedAt`; idle reset freshness uses `lastInteractionAt`. Background/system-event writes such as heartbeat, cron wakeups, exec notifications, and gateway bookkeeping can update `updatedAt`, but they do not keep daily/idle sessions fresh.
|
||||
- **`resetByType`**: per-type overrides (`direct`, `group`, `thread`). Legacy `dm` accepted as alias for `direct`.
|
||||
- **`parentForkMaxTokens`**: max parent-session `totalTokens` allowed when creating a forked thread session (default `100000`).
|
||||
- If parent `totalTokens` is above this value, OpenClaw starts a fresh thread session instead of inheriting parent transcript history.
|
||||
@@ -1332,6 +1346,7 @@ Defaults for Talk mode (macOS/iOS/Android).
|
||||
},
|
||||
system: {},
|
||||
},
|
||||
speechLocale: "ru-RU",
|
||||
silenceTimeoutMs: 1500,
|
||||
interruptOnSpeech: true,
|
||||
},
|
||||
@@ -1346,6 +1361,7 @@ Defaults for Talk mode (macOS/iOS/Android).
|
||||
- `providers.*.voiceAliases` lets Talk directives use friendly names.
|
||||
- `providers.mlx.modelId` selects the Hugging Face repo used by the macOS local MLX helper. If omitted, macOS uses `mlx-community/Soprano-80M-bf16`.
|
||||
- macOS MLX playback runs through the bundled `openclaw-mlx-tts` helper when present, or an executable on `PATH`; `OPENCLAW_MLX_TTS_BIN` overrides the helper path for development.
|
||||
- `speechLocale` sets the BCP 47 locale id used by iOS/macOS Talk speech recognition. Leave unset to use the device default.
|
||||
- `silenceTimeoutMs` controls how long Talk mode waits after user silence before it sends the transcript. Unset keeps the platform default pause window (`700 ms on macOS and Android, 900 ms on iOS`).
|
||||
|
||||
---
|
||||
|
||||
@@ -43,6 +43,7 @@ Moved to a dedicated page — see
|
||||
- `session.*` (session lifecycle, compaction, pruning)
|
||||
- `messages.*` (message delivery, TTS, markdown rendering)
|
||||
- `talk.*` (Talk mode)
|
||||
- `talk.speechLocale`: optional BCP 47 locale id for Talk speech recognition on iOS/macOS
|
||||
- `talk.silenceTimeoutMs`: when unset, Talk keeps the platform default pause window before sending the transcript (`700 ms on macOS and Android, 900 ms on iOS`)
|
||||
|
||||
## Tools and custom providers
|
||||
@@ -159,7 +160,7 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
|
||||
- `plugins.entries.<id>.apiKey`: plugin-level API key convenience field (when supported by the plugin).
|
||||
- `plugins.entries.<id>.env`: plugin-scoped env var map.
|
||||
- `plugins.entries.<id>.hooks.allowPromptInjection`: when `false`, core blocks `before_prompt_build` and ignores prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride`. Applies to native plugin hooks and supported bundle-provided hook directories.
|
||||
- `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`, and `agent_end`.
|
||||
- `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_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>.config`: plugin-defined config object (validated by native OpenClaw plugin schema when available).
|
||||
@@ -186,9 +187,6 @@ See [MCP](/cli/mcp#openclaw-as-an-mcp-client-registry) and
|
||||
- Enabled Claude bundle plugins can also contribute embedded Pi defaults from `settings.json`; OpenClaw applies those as sanitized agent settings, not as raw OpenClaw config patches.
|
||||
- `plugins.slots.memory`: pick the active memory plugin id, or `"none"` to disable memory plugins.
|
||||
- `plugins.slots.contextEngine`: pick the active context engine plugin id; defaults to `"legacy"` unless you install and select another engine.
|
||||
- `plugins.installs`: CLI-managed install metadata used by `openclaw plugins update`.
|
||||
- Includes `source`, `spec`, `sourcePath`, `installPath`, `version`, `resolvedName`, `resolvedVersion`, `resolvedSpec`, `integrity`, `shasum`, `resolvedAt`, `installedAt`.
|
||||
- Treat `plugins.installs.*` as managed state; prefer CLI commands over manual edits.
|
||||
|
||||
See [Plugins](/tools/plugin).
|
||||
|
||||
@@ -253,6 +251,12 @@ See [Plugins](/tools/plugin).
|
||||
- `profiles.*.cdpUrl` accepts `http://`, `https://`, `ws://`, and `wss://`.
|
||||
Use HTTP(S) when you want OpenClaw to discover `/json/version`; use WS(S)
|
||||
when your provider gives you a direct DevTools WebSocket URL.
|
||||
- `remoteCdpTimeoutMs` and `remoteCdpHandshakeTimeoutMs` apply to remote and
|
||||
`attachOnly` CDP reachability plus tab-opening requests. Managed loopback
|
||||
profiles keep local CDP defaults.
|
||||
- If an externally managed CDP service is reachable through loopback, set that
|
||||
profile's `attachOnly: true`; otherwise OpenClaw treats the loopback port as a
|
||||
local managed browser profile and may report local port ownership errors.
|
||||
- `existing-session` profiles use Chrome MCP instead of CDP and can attach on
|
||||
the selected host or through a connected browser node.
|
||||
- `existing-session` profiles can set `userDataDir` to target a specific
|
||||
@@ -269,9 +273,12 @@ See [Plugins](/tools/plugin).
|
||||
- Local managed profiles use `browser.localLaunchTimeoutMs` for Chrome CDP HTTP
|
||||
discovery after process start and `browser.localCdpReadyTimeoutMs` for
|
||||
post-launch CDP websocket readiness. Raise them on slower hosts where Chrome
|
||||
starts successfully but readiness checks race startup.
|
||||
starts successfully but readiness checks race startup. Both values must be
|
||||
positive integers up to `120000` ms; invalid config values are rejected.
|
||||
- Auto-detect order: default browser if Chromium-based → Chrome → Brave → Edge → Chromium → Chrome Canary.
|
||||
- `browser.executablePath` accepts `~` for your OS home directory.
|
||||
- `browser.executablePath` and `browser.profiles.<name>.executablePath` both
|
||||
accept `~` and `~/...` for your OS home directory before Chromium launch.
|
||||
Per-profile `userDataDir` on `existing-session` profiles is also tilde-expanded.
|
||||
- Control service: loopback only (port derived from `gateway.port`, default `18791`).
|
||||
- `extraArgs` appends extra launch flags to local Chromium startup (for example
|
||||
`--disable-gpu`, window sizing, or debug flags).
|
||||
@@ -862,6 +869,9 @@ Notes:
|
||||
otel: {
|
||||
enabled: false,
|
||||
endpoint: "https://otel-collector.example.com:4318",
|
||||
tracesEndpoint: "https://traces.example.com/v1/traces",
|
||||
metricsEndpoint: "https://metrics.example.com/v1/metrics",
|
||||
logsEndpoint: "https://logs.example.com/v1/logs",
|
||||
protocol: "http/protobuf", // http/protobuf | grpc
|
||||
headers: { "x-tenant-id": "my-org" },
|
||||
serviceName: "openclaw-gateway",
|
||||
@@ -894,8 +904,9 @@ Notes:
|
||||
- `enabled`: master toggle for instrumentation output (default: `true`).
|
||||
- `flags`: array of flag strings enabling targeted log output (supports wildcards like `"telegram.*"` or `"*"`).
|
||||
- `stuckSessionWarnMs`: age threshold in ms for emitting stuck-session warnings while a session remains in processing state.
|
||||
- `otel.enabled`: enables the OpenTelemetry export pipeline (default: `false`).
|
||||
- `otel.enabled`: enables the OpenTelemetry export pipeline (default: `false`). For the full configuration, signal catalog, and privacy model, see [OpenTelemetry export](/gateway/opentelemetry).
|
||||
- `otel.endpoint`: collector URL for OTel export.
|
||||
- `otel.tracesEndpoint` / `otel.metricsEndpoint` / `otel.logsEndpoint`: optional signal-specific OTLP endpoints. When set, they override `otel.endpoint` for that signal only.
|
||||
- `otel.protocol`: `"http/protobuf"` (default) or `"grpc"`.
|
||||
- `otel.headers`: extra HTTP/gRPC metadata headers sent with OTel export requests.
|
||||
- `otel.serviceName`: service name for resource attributes.
|
||||
@@ -903,7 +914,9 @@ Notes:
|
||||
- `otel.sampleRate`: trace sampling rate `0`–`1`.
|
||||
- `otel.flushIntervalMs`: periodic telemetry flush interval in ms.
|
||||
- `otel.captureContent`: opt-in raw content capture for OTEL span attributes. Defaults to off. Boolean `true` captures non-system message/tool content; the object form lets you enable `inputMessages`, `outputMessages`, `toolInputs`, `toolOutputs`, and `systemPrompt` explicitly.
|
||||
- `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`: environment toggle for latest experimental GenAI span provider attributes. By default spans keep the legacy `gen_ai.system` attribute for compatibility; GenAI metrics use bounded semantic attributes.
|
||||
- `OPENCLAW_OTEL_PRELOADED=1`: environment toggle for hosts that already registered a global OpenTelemetry SDK. OpenClaw then skips plugin-owned SDK startup/shutdown while keeping diagnostic listeners active.
|
||||
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, and `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`: signal-specific endpoint env vars used when the matching config key is unset.
|
||||
- `cacheTrace.enabled`: log cache trace snapshots for embedded runs (default: `false`).
|
||||
- `cacheTrace.filePath`: output path for cache trace JSONL (default: `$OPENCLAW_STATE_DIR/logs/cache-trace.jsonl`).
|
||||
- `cacheTrace.includeMessages` / `includePrompt` / `includeSystem`: control what is included in cache trace output (all default: `true`).
|
||||
@@ -942,7 +955,7 @@ Notes:
|
||||
```json5
|
||||
{
|
||||
acp: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
dispatch: { enabled: true },
|
||||
backend: "acpx",
|
||||
defaultAgent: "main",
|
||||
@@ -966,9 +979,10 @@ Notes:
|
||||
}
|
||||
```
|
||||
|
||||
- `enabled`: global ACP feature gate (default: `false`).
|
||||
- `enabled`: global ACP feature gate (default: `true`; set `false` to hide ACP dispatch and spawn affordances).
|
||||
- `dispatch.enabled`: independent gate for ACP session turn dispatch (default: `true`). Set `false` to keep ACP commands available while blocking execution.
|
||||
- `backend`: default ACP runtime backend id (must match a registered ACP runtime plugin).
|
||||
If `plugins.allow` is set, include the backend plugin id (for example `acpx`) or the bundled default plugin will not load.
|
||||
- `defaultAgent`: fallback ACP target agent id when spawns do not specify an explicit target.
|
||||
- `allowedAgents`: allowlist of agent ids permitted for ACP runtime sessions; empty means no additional restriction.
|
||||
- `maxConcurrentSessions`: maximum concurrently active ACP sessions.
|
||||
|
||||
@@ -129,9 +129,10 @@ diagnostic event collection:
|
||||
Disabling diagnostics reduces bug-report detail. It does not affect normal
|
||||
Gateway logging.
|
||||
|
||||
## Related docs
|
||||
## Related
|
||||
|
||||
- [Health Checks](/gateway/health)
|
||||
- [Health checks](/gateway/health)
|
||||
- [Gateway CLI](/cli/gateway#gateway-diagnostics-export)
|
||||
- [Gateway Protocol](/gateway/protocol#system-and-identity)
|
||||
- [Gateway protocol](/gateway/protocol#system-and-identity)
|
||||
- [Logging](/logging)
|
||||
- [OpenTelemetry export](/gateway/opentelemetry) — separate flow for streaming diagnostics to a collector
|
||||
|
||||
@@ -70,6 +70,7 @@ cat ~/.openclaw/openclaw.json
|
||||
- Legacy plugin manifest contract key migration (`speechProviders`, `realtimeTranscriptionProviders`, `realtimeVoiceProviders`, `mediaUnderstandingProviders`, `imageGenerationProviders`, `videoGenerationProviders`, `webFetchProviders`, `webSearchProviders` → `contracts`).
|
||||
- Legacy cron store migration (`jobId`, `schedule.cron`, top-level delivery/payload fields, payload `provider`, simple `notify: true` webhook fallback jobs).
|
||||
- Session lock file inspection and stale lock cleanup.
|
||||
- Session transcript repair for duplicated prompt-rewrite branches created by affected 2026.4.24 builds.
|
||||
- State integrity and permissions checks (sessions, transcripts, state dir).
|
||||
- Config file permission checks (chmod 600) when running locally.
|
||||
- Model auth health: checks OAuth expiry, can refresh expiring tokens, and reports auth-profile cooldown/disabled states.
|
||||
@@ -317,6 +318,15 @@ considered stale (dead PID or older than 30 minutes). In `--fix` / `--repair`
|
||||
mode it removes stale lock files automatically; otherwise it prints a note and
|
||||
instructs you to rerun with `--fix`.
|
||||
|
||||
### 3d) Session transcript branch repair
|
||||
|
||||
Doctor scans agent session JSONL files for the duplicated branch shape created
|
||||
by the 2026.4.24 prompt transcript rewrite bug: an abandoned user turn with
|
||||
OpenClaw internal runtime context plus an active sibling containing the same
|
||||
visible user prompt. In `--fix` / `--repair` mode, doctor backs up each affected
|
||||
file next to the original and rewrites the transcript to the active branch so
|
||||
gateway history and memory readers no longer see duplicate turns.
|
||||
|
||||
### 4) State integrity checks (session persistence, routing, and safety)
|
||||
|
||||
The state directory is the operational brainstem. If it vanishes, you lose
|
||||
@@ -457,7 +467,7 @@ Doctor prints a summary of the workspace state for the default agent:
|
||||
- **Skills status**: counts eligible, missing-requirements, and allowlist-blocked skills.
|
||||
- **Legacy workspace dirs**: warns when `~/openclaw` or other legacy workspace directories
|
||||
exist alongside the current workspace.
|
||||
- **Plugin status**: counts loaded/disabled/errored plugins; lists plugin IDs for any
|
||||
- **Plugin status**: counts enabled/disabled/errored plugins; lists plugin IDs for any
|
||||
errors; reports bundle plugin capabilities.
|
||||
- **Plugin compatibility warnings**: flags plugins that have compatibility issues with
|
||||
the current runtime.
|
||||
|
||||
@@ -263,8 +263,9 @@ Use `accountId` to target a specific account on multi-account channels like Tele
|
||||
- If the resolved heartbeat target supports typing, OpenClaw shows typing while
|
||||
the heartbeat run is active. This uses the same target the heartbeat would
|
||||
send chat output to, and it is disabled by `typingMode: "never"`.
|
||||
- Heartbeat-only replies do **not** keep the session alive; the last `updatedAt`
|
||||
is restored so idle expiry behaves normally.
|
||||
- Heartbeat-only replies do **not** keep the session alive. Heartbeat metadata
|
||||
may update the session row, but idle expiry uses `lastInteractionAt` from the
|
||||
last real user/channel message, and daily expiry uses `sessionStartedAt`.
|
||||
- Control UI and WebChat history hide heartbeat prompts and OK-only
|
||||
acknowledgments. The underlying session transcript can still contain those
|
||||
turns for audit/replay.
|
||||
|
||||
@@ -112,6 +112,12 @@ All of these run on the main Gateway port and use the same trusted operator auth
|
||||
| Gateway port | `--port` → `OPENCLAW_GATEWAY_PORT` → `gateway.port` → `18789` |
|
||||
| Bind mode | CLI/override → `gateway.bind` → `loopback` |
|
||||
|
||||
Gateway startup uses the same effective port and bind when it seeds local
|
||||
Control UI origins for non-loopback binds. For example, `--bind lan --port 3000`
|
||||
seeds `http://localhost:3000` and `http://127.0.0.1:3000` before runtime
|
||||
validation runs. Add any remote browser origins, such as HTTPS proxy URLs, to
|
||||
`gateway.controlUi.allowedOrigins` explicitly.
|
||||
|
||||
### Hot reload modes
|
||||
|
||||
| `gateway.reload.mode` | Behavior |
|
||||
|
||||
@@ -114,5 +114,6 @@ This keeps existing file logs stable while making interactive output scannable.
|
||||
|
||||
## Related
|
||||
|
||||
- [Logging overview](/logging)
|
||||
- [Logging](/logging)
|
||||
- [OpenTelemetry export](/gateway/opentelemetry)
|
||||
- [Diagnostics export](/gateway/diagnostics)
|
||||
|
||||
308
docs/gateway/opentelemetry.md
Normal file
308
docs/gateway/opentelemetry.md
Normal file
@@ -0,0 +1,308 @@
|
||||
---
|
||||
summary: "Export OpenClaw diagnostics to any OpenTelemetry collector via the diagnostics-otel plugin (OTLP/HTTP)"
|
||||
title: "OpenTelemetry export"
|
||||
read_when:
|
||||
- You want to send OpenClaw model usage, message flow, or session metrics to an OpenTelemetry collector
|
||||
- You are wiring traces, metrics, or logs into Grafana, Datadog, Honeycomb, New Relic, Tempo, or another OTLP backend
|
||||
- You need the exact metric names, span names, or attribute shapes to build dashboards or alerts
|
||||
---
|
||||
|
||||
OpenClaw exports diagnostics through the bundled `diagnostics-otel` plugin
|
||||
using **OTLP/HTTP (protobuf)**. Any collector or backend that accepts OTLP/HTTP
|
||||
works without code changes. For local file logs and how to read them, see
|
||||
[Logging](/logging).
|
||||
|
||||
## How it fits together
|
||||
|
||||
- **Diagnostics events** are structured, in-process records emitted by the
|
||||
Gateway and bundled plugins for model runs, message flow, sessions, queues,
|
||||
and exec.
|
||||
- **`diagnostics-otel` plugin** subscribes to those events and exports them as
|
||||
OpenTelemetry **metrics**, **traces**, and **logs** over OTLP/HTTP.
|
||||
- Exporters only attach when both the diagnostics surface and the plugin are
|
||||
enabled, so the in-process cost stays near zero by default.
|
||||
|
||||
## Quick start
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
allow: ["diagnostics-otel"],
|
||||
entries: {
|
||||
"diagnostics-otel": { enabled: true },
|
||||
},
|
||||
},
|
||||
diagnostics: {
|
||||
enabled: true,
|
||||
otel: {
|
||||
enabled: true,
|
||||
endpoint: "http://otel-collector:4318",
|
||||
protocol: "http/protobuf",
|
||||
serviceName: "openclaw-gateway",
|
||||
traces: true,
|
||||
metrics: true,
|
||||
logs: true,
|
||||
sampleRate: 0.2,
|
||||
flushIntervalMs: 60000,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
You can also enable the plugin from the CLI:
|
||||
|
||||
```bash
|
||||
openclaw plugins enable diagnostics-otel
|
||||
```
|
||||
|
||||
<Note>
|
||||
`protocol` currently supports `http/protobuf` only. `grpc` is ignored.
|
||||
</Note>
|
||||
|
||||
## Signals exported
|
||||
|
||||
| Signal | What goes in it |
|
||||
| ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Metrics** | Counters and histograms for token usage, cost, run duration, message flow, queue lanes, session state, exec, and memory pressure. |
|
||||
| **Traces** | Spans for model usage, model calls, tool execution, exec, webhook/message processing, context assembly, and tool loops. |
|
||||
| **Logs** | Structured `logging.file` records exported over OTLP when `diagnostics.otel.logs` is enabled. |
|
||||
|
||||
Toggle `traces`, `metrics`, and `logs` independently. All three default to on
|
||||
when `diagnostics.otel.enabled` is true.
|
||||
|
||||
## Configuration reference
|
||||
|
||||
```json5
|
||||
{
|
||||
diagnostics: {
|
||||
enabled: true,
|
||||
otel: {
|
||||
enabled: true,
|
||||
endpoint: "http://otel-collector:4318",
|
||||
tracesEndpoint: "http://otel-collector:4318/v1/traces",
|
||||
metricsEndpoint: "http://otel-collector:4318/v1/metrics",
|
||||
logsEndpoint: "http://otel-collector:4318/v1/logs",
|
||||
protocol: "http/protobuf", // grpc is ignored
|
||||
serviceName: "openclaw-gateway",
|
||||
headers: { "x-collector-token": "..." },
|
||||
traces: true,
|
||||
metrics: true,
|
||||
logs: true,
|
||||
sampleRate: 0.2, // root-span sampler, 0.0..1.0
|
||||
flushIntervalMs: 60000, // metric export interval (min 1000ms)
|
||||
captureContent: {
|
||||
enabled: false,
|
||||
inputMessages: false,
|
||||
outputMessages: false,
|
||||
toolInputs: false,
|
||||
toolOutputs: false,
|
||||
systemPrompt: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Variable | Purpose |
|
||||
| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Override `diagnostics.otel.endpoint`. If the value already contains `/v1/traces`, `/v1/metrics`, or `/v1/logs`, it is used as-is. |
|
||||
| `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` / `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` / `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT` | Signal-specific endpoint overrides used when the matching `diagnostics.otel.*Endpoint` config key is unset. Signal-specific config wins over signal-specific env, which wins over the shared endpoint. |
|
||||
| `OTEL_SERVICE_NAME` | Override `diagnostics.otel.serviceName`. |
|
||||
| `OTEL_EXPORTER_OTLP_PROTOCOL` | Override the wire protocol (only `http/protobuf` is honored today). |
|
||||
| `OTEL_SEMCONV_STABILITY_OPT_IN` | Set to `gen_ai_latest_experimental` to emit the latest experimental GenAI span attribute (`gen_ai.provider.name`) instead of the legacy `gen_ai.system`. GenAI metrics always use bounded, low-cardinality semantic attributes regardless. |
|
||||
| `OPENCLAW_OTEL_PRELOADED` | Set to `1` when another preload or host process already registered the global OpenTelemetry SDK. The plugin then skips its own NodeSDK lifecycle but still wires diagnostic listeners and honors `traces`/`metrics`/`logs`. |
|
||||
|
||||
## Privacy and content capture
|
||||
|
||||
Raw model/tool content is **not** exported by default. Spans carry bounded
|
||||
identifiers (channel, provider, model, error category, hash-only request ids)
|
||||
and never include prompt text, response text, tool inputs, tool outputs, or
|
||||
session keys.
|
||||
|
||||
Set `diagnostics.otel.captureContent.*` to `true` only when your collector and
|
||||
retention policy are approved for prompt, response, tool, or system-prompt
|
||||
text. Each subkey is opt-in independently:
|
||||
|
||||
- `inputMessages` — user prompt content.
|
||||
- `outputMessages` — model response content.
|
||||
- `toolInputs` — tool argument payloads.
|
||||
- `toolOutputs` — tool result payloads.
|
||||
- `systemPrompt` — assembled system/developer prompt.
|
||||
|
||||
When any subkey is enabled, model and tool spans get bounded, redacted
|
||||
`openclaw.content.*` attributes for that class only.
|
||||
|
||||
## Sampling and flushing
|
||||
|
||||
- **Traces:** `diagnostics.otel.sampleRate` (root-span only, `0.0` drops all,
|
||||
`1.0` keeps all).
|
||||
- **Metrics:** `diagnostics.otel.flushIntervalMs` (minimum `1000`).
|
||||
- **Logs:** OTLP logs respect `logging.level` (file log level). Console
|
||||
redaction does **not** apply to OTLP logs. High-volume installs should
|
||||
prefer OTLP collector sampling/filtering over local sampling.
|
||||
|
||||
## Exported metrics
|
||||
|
||||
### Model usage
|
||||
|
||||
- `openclaw.tokens` (counter, attrs: `openclaw.token`, `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
- `openclaw.cost.usd` (counter, attrs: `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
- `openclaw.run.duration_ms` (histogram, attrs: `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
- `openclaw.context.tokens` (histogram, attrs: `openclaw.context`, `openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
- `gen_ai.client.token.usage` (histogram, GenAI semantic-conventions metric, attrs: `gen_ai.token.type` = `input`/`output`, `gen_ai.provider.name`, `gen_ai.operation.name`, `gen_ai.request.model`)
|
||||
- `gen_ai.client.operation.duration` (histogram, seconds, GenAI semantic-conventions metric, attrs: `gen_ai.provider.name`, `gen_ai.operation.name`, `gen_ai.request.model`, optional `error.type`)
|
||||
|
||||
### Message flow
|
||||
|
||||
- `openclaw.webhook.received` (counter, attrs: `openclaw.channel`, `openclaw.webhook`)
|
||||
- `openclaw.webhook.error` (counter, attrs: `openclaw.channel`, `openclaw.webhook`)
|
||||
- `openclaw.webhook.duration_ms` (histogram, attrs: `openclaw.channel`, `openclaw.webhook`)
|
||||
- `openclaw.message.queued` (counter, attrs: `openclaw.channel`, `openclaw.source`)
|
||||
- `openclaw.message.processed` (counter, attrs: `openclaw.channel`, `openclaw.outcome`)
|
||||
- `openclaw.message.duration_ms` (histogram, attrs: `openclaw.channel`, `openclaw.outcome`)
|
||||
- `openclaw.message.delivery.started` (counter, attrs: `openclaw.channel`, `openclaw.delivery.kind`)
|
||||
- `openclaw.message.delivery.duration_ms` (histogram, attrs: `openclaw.channel`, `openclaw.delivery.kind`, `openclaw.outcome`, `openclaw.errorCategory`)
|
||||
|
||||
### Queues and sessions
|
||||
|
||||
- `openclaw.queue.lane.enqueue` (counter, attrs: `openclaw.lane`)
|
||||
- `openclaw.queue.lane.dequeue` (counter, attrs: `openclaw.lane`)
|
||||
- `openclaw.queue.depth` (histogram, attrs: `openclaw.lane` or `openclaw.channel=heartbeat`)
|
||||
- `openclaw.queue.wait_ms` (histogram, attrs: `openclaw.lane`)
|
||||
- `openclaw.session.state` (counter, attrs: `openclaw.state`, `openclaw.reason`)
|
||||
- `openclaw.session.stuck` (counter, attrs: `openclaw.state`)
|
||||
- `openclaw.session.stuck_age_ms` (histogram, attrs: `openclaw.state`)
|
||||
- `openclaw.run.attempt` (counter, attrs: `openclaw.attempt`)
|
||||
|
||||
### Exec
|
||||
|
||||
- `openclaw.exec.duration_ms` (histogram, attrs: `openclaw.exec.target`, `openclaw.exec.mode`, `openclaw.outcome`, `openclaw.failureKind`)
|
||||
|
||||
### Diagnostics internals (memory and tool loop)
|
||||
|
||||
- `openclaw.memory.heap_used_bytes` (histogram, attrs: `openclaw.memory.kind`)
|
||||
- `openclaw.memory.rss_bytes` (histogram)
|
||||
- `openclaw.memory.pressure` (counter, attrs: `openclaw.memory.level`)
|
||||
- `openclaw.tool.loop.iterations` (counter, attrs: `openclaw.toolName`, `openclaw.outcome`)
|
||||
- `openclaw.tool.loop.duration_ms` (histogram, attrs: `openclaw.toolName`, `openclaw.outcome`)
|
||||
|
||||
## Exported spans
|
||||
|
||||
- `openclaw.model.usage`
|
||||
- `openclaw.channel`, `openclaw.provider`, `openclaw.model`
|
||||
- `openclaw.tokens.*` (input/output/cache_read/cache_write/total)
|
||||
- `gen_ai.system` by default, or `gen_ai.provider.name` when the latest GenAI semantic conventions are opted in
|
||||
- `gen_ai.request.model`, `gen_ai.operation.name`, `gen_ai.usage.*`
|
||||
- `openclaw.run`
|
||||
- `openclaw.outcome`, `openclaw.channel`, `openclaw.provider`, `openclaw.model`, `openclaw.errorCategory`
|
||||
- `openclaw.model.call`
|
||||
- `gen_ai.system` by default, or `gen_ai.provider.name` when the latest GenAI semantic conventions are opted in
|
||||
- `gen_ai.request.model`, `gen_ai.operation.name`, `openclaw.provider`, `openclaw.model`, `openclaw.api`, `openclaw.transport`
|
||||
- `openclaw.provider.request_id_hash` (bounded SHA-based hash of the upstream provider request id; raw ids are not exported)
|
||||
- `openclaw.tool.execution`
|
||||
- `gen_ai.tool.name`, `openclaw.toolName`, `openclaw.errorCategory`, `openclaw.tool.params.*`
|
||||
- `openclaw.exec`
|
||||
- `openclaw.exec.target`, `openclaw.exec.mode`, `openclaw.outcome`, `openclaw.failureKind`, `openclaw.exec.command_length`, `openclaw.exec.exit_code`, `openclaw.exec.timed_out`
|
||||
- `openclaw.webhook.processed`
|
||||
- `openclaw.channel`, `openclaw.webhook`, `openclaw.chatId`
|
||||
- `openclaw.webhook.error`
|
||||
- `openclaw.channel`, `openclaw.webhook`, `openclaw.chatId`, `openclaw.error`
|
||||
- `openclaw.message.processed`
|
||||
- `openclaw.channel`, `openclaw.outcome`, `openclaw.chatId`, `openclaw.messageId`, `openclaw.reason`
|
||||
- `openclaw.message.delivery`
|
||||
- `openclaw.channel`, `openclaw.delivery.kind`, `openclaw.outcome`, `openclaw.errorCategory`, `openclaw.delivery.result_count`
|
||||
- `openclaw.session.stuck`
|
||||
- `openclaw.state`, `openclaw.ageMs`, `openclaw.queueDepth`
|
||||
- `openclaw.context.assembled`
|
||||
- `openclaw.prompt.size`, `openclaw.history.size`, `openclaw.context.tokens`, `openclaw.errorCategory` (no prompt, history, response, or session-key content)
|
||||
- `openclaw.tool.loop`
|
||||
- `openclaw.toolName`, `openclaw.outcome`, `openclaw.iterations`, `openclaw.errorCategory` (no loop messages, params, or tool output)
|
||||
- `openclaw.memory.pressure`
|
||||
- `openclaw.memory.level`, `openclaw.memory.heap_used_bytes`, `openclaw.memory.rss_bytes`
|
||||
|
||||
When content capture is explicitly enabled, model and tool spans can also
|
||||
include bounded, redacted `openclaw.content.*` attributes for the specific
|
||||
content classes you opted into.
|
||||
|
||||
## Diagnostic event catalog
|
||||
|
||||
The events below back the metrics and spans above. Plugins can also subscribe
|
||||
to them directly without OTLP export.
|
||||
|
||||
**Model usage**
|
||||
|
||||
- `model.usage` — tokens, cost, duration, context, provider/model/channel,
|
||||
session ids. `usage` is provider/turn accounting for cost and telemetry;
|
||||
`context.used` is the current prompt/context snapshot and can be lower than
|
||||
provider `usage.total` when cached input or tool-loop calls are involved.
|
||||
|
||||
**Message flow**
|
||||
|
||||
- `webhook.received` / `webhook.processed` / `webhook.error`
|
||||
- `message.queued` / `message.processed`
|
||||
- `message.delivery.started` / `message.delivery.completed` / `message.delivery.error`
|
||||
|
||||
**Queue and session**
|
||||
|
||||
- `queue.lane.enqueue` / `queue.lane.dequeue`
|
||||
- `session.state` / `session.stuck`
|
||||
- `run.attempt`
|
||||
- `diagnostic.heartbeat` (aggregate counters: webhooks/queue/session)
|
||||
|
||||
**Exec**
|
||||
|
||||
- `exec.process.completed` — terminal outcome, duration, target, mode, exit
|
||||
code, and failure kind. Command text and working directories are not
|
||||
included.
|
||||
|
||||
## Without an exporter
|
||||
|
||||
You can keep diagnostics events available to plugins or custom sinks without
|
||||
running `diagnostics-otel`:
|
||||
|
||||
```json5
|
||||
{
|
||||
diagnostics: { enabled: true },
|
||||
}
|
||||
```
|
||||
|
||||
For targeted debug output without raising `logging.level`, use diagnostics
|
||||
flags. Flags are case-insensitive and support wildcards (e.g. `telegram.*` or
|
||||
`*`):
|
||||
|
||||
```json5
|
||||
{
|
||||
diagnostics: { flags: ["telegram.http"] },
|
||||
}
|
||||
```
|
||||
|
||||
Or as a one-off env override:
|
||||
|
||||
```bash
|
||||
OPENCLAW_DIAGNOSTICS=telegram.http,telegram.payload openclaw gateway
|
||||
```
|
||||
|
||||
Flag output goes to the standard log file (`logging.file`) and is still
|
||||
redacted by `logging.redactSensitive`. Full guide:
|
||||
[Diagnostics flags](/diagnostics/flags).
|
||||
|
||||
## Disable
|
||||
|
||||
```json5
|
||||
{
|
||||
diagnostics: { otel: { enabled: false } },
|
||||
}
|
||||
```
|
||||
|
||||
You can also leave `diagnostics-otel` out of `plugins.allow`, or run
|
||||
`openclaw plugins disable diagnostics-otel`.
|
||||
|
||||
## Related
|
||||
|
||||
- [Logging](/logging) — file logs, console output, CLI tailing, and the Control UI Logs tab
|
||||
- [Gateway logging internals](/gateway/logging) — WS log styles, subsystem prefixes, and console capture
|
||||
- [Diagnostics flags](/diagnostics/flags) — targeted debug-log flags
|
||||
- [Diagnostics export](/gateway/diagnostics) — operator support-bundle tool (separate from OTEL export)
|
||||
- [Configuration reference](/gateway/configuration-reference#diagnostics) — full `diagnostics.*` field reference
|
||||
@@ -148,11 +148,12 @@ Security boundary:
|
||||
When an already paired device reconnects with only non-sensitive metadata
|
||||
changes (for example, display name or client platform hints), OpenClaw treats
|
||||
that as a `metadata-upgrade`. Silent auto-approval is narrow: it applies only
|
||||
to trusted local CLI/helper reconnects that already proved possession of the
|
||||
shared token or password over loopback. Browser/Control UI clients and remote
|
||||
clients still use the explicit re-approval flow. Scope upgrades (read to
|
||||
write/admin) and public key changes are **not** eligible for metadata-upgrade
|
||||
auto-approval — they stay as explicit re-approval requests.
|
||||
to trusted non-browser local reconnects that already proved possession of local
|
||||
or shared credentials, including same-host native app reconnects after OS
|
||||
version metadata changes. Browser/Control UI clients and remote clients still
|
||||
use the explicit re-approval flow. Scope upgrades (read to write/admin) and
|
||||
public key changes are **not** eligible for metadata-upgrade auto-approval —
|
||||
they stay as explicit re-approval requests.
|
||||
|
||||
## QR pairing helpers
|
||||
|
||||
|
||||
@@ -110,6 +110,14 @@ permissions:
|
||||
}
|
||||
```
|
||||
|
||||
Trusted same-process backend clients (`client.id: "gateway-client"`,
|
||||
`client.mode: "backend"`) may omit `device` on direct loopback connections when
|
||||
they authenticate with the shared gateway token/password. This path is reserved
|
||||
for internal control-plane RPCs and keeps stale CLI/device pairing baselines from
|
||||
blocking local backend work such as subagent session updates. Remote clients,
|
||||
browser-origin clients, node clients, and explicit device-token/device-identity
|
||||
clients still use the normal pairing and scope-upgrade checks.
|
||||
|
||||
When a device token is issued, `hello-ok` also includes:
|
||||
|
||||
```json
|
||||
@@ -569,11 +577,13 @@ rather than the pre-handshake defaults.
|
||||
trusted shared-secret helper flows.
|
||||
- Same-host tailnet or LAN connects are still treated as remote for pairing and
|
||||
require approval.
|
||||
- All WS clients must include `device` identity during `connect` (operator + node).
|
||||
Control UI can omit it only in these modes:
|
||||
- WS clients normally include `device` identity during `connect` (operator +
|
||||
node). The only device-less operator exceptions are explicit trust paths:
|
||||
- `gateway.controlUi.allowInsecureAuth=true` for localhost-only insecure HTTP compatibility.
|
||||
- successful `gateway.auth.mode: "trusted-proxy"` operator Control UI auth.
|
||||
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true` (break-glass, severe security downgrade).
|
||||
- direct-loopback `gateway-client` backend RPCs authenticated with the shared
|
||||
gateway token/password.
|
||||
- All connections must sign the server-provided `connect.challenge` nonce.
|
||||
|
||||
### Device auth migration diagnostics
|
||||
|
||||
@@ -97,9 +97,9 @@ exhaustive):
|
||||
| `tools.exec.safe_bin_trusted_dirs_risky` | warn | `safeBinTrustedDirs` includes mutable or risky directories | `tools.exec.safeBinTrustedDirs`, `agents.list[].tools.exec.safeBinTrustedDirs` | no |
|
||||
| `skills.workspace.symlink_escape` | warn | Workspace `skills/**/SKILL.md` resolves outside workspace root (symlink-chain drift) | workspace `skills/**` filesystem state | no |
|
||||
| `plugins.extensions_no_allowlist` | warn | Plugins are installed without an explicit plugin allowlist | `plugins.allowlist` | no |
|
||||
| `plugins.installs_unpinned_npm_specs` | warn | Plugin install records are not pinned to immutable npm specs | plugin install metadata | no |
|
||||
| `plugins.installs_missing_integrity` | warn | Plugin install records lack integrity metadata | plugin install metadata | no |
|
||||
| `plugins.installs_version_drift` | warn | Plugin install records drift from installed packages | plugin install metadata | no |
|
||||
| `plugins.installs_unpinned_npm_specs` | warn | Plugin index records are not pinned to immutable npm specs | plugin install metadata | no |
|
||||
| `plugins.installs_missing_integrity` | warn | Plugin index records lack integrity metadata | plugin install metadata | no |
|
||||
| `plugins.installs_version_drift` | warn | Plugin index records drift from installed packages | plugin install metadata | no |
|
||||
| `plugins.code_safety` | warn/critical | Plugin code scan found suspicious or dangerous patterns | plugin code / install source | no |
|
||||
| `plugins.code_safety.entry_path` | warn | Plugin entry path points into hidden or `node_modules` locations | plugin manifest `entry` | no |
|
||||
| `plugins.code_safety.entry_escape` | critical | Plugin entry escapes the plugin directory | plugin manifest `entry` | no |
|
||||
|
||||
@@ -92,6 +92,11 @@ Treat Gateway and node as one operator trust domain, with different roles:
|
||||
- **Gateway** is the control plane and policy surface (`gateway.auth`, tool policy, routing).
|
||||
- **Node** is remote execution surface paired to that Gateway (commands, device actions, host-local capabilities).
|
||||
- A caller authenticated to the Gateway is trusted at Gateway scope. After pairing, node actions are trusted operator actions on that node.
|
||||
- Direct loopback backend clients authenticated with the shared gateway
|
||||
token/password can make internal control-plane RPCs without presenting a user
|
||||
device identity. This is not a remote or browser pairing bypass: network
|
||||
clients, node clients, device-token clients, and explicit device identities
|
||||
still go through pairing and scope-upgrade enforcement.
|
||||
- `sessionKey` is routing/context selection, not per-user auth.
|
||||
- Exec approvals (allowlist + ask) are guardrails for operator intent, not hostile multi-tenant isolation.
|
||||
- OpenClaw's product default for trusted single-operator setups is that host exec on `gateway`/`node` is allowed without approval prompts (`security="full"`, `ask="off"` unless you tighten it). That default is intentional UX, not a vulnerability by itself.
|
||||
@@ -504,7 +509,7 @@ Plugins run **in-process** with the Gateway. Treat them as trusted code:
|
||||
- If you install or update plugins (`openclaw plugins install <package>`, `openclaw plugins update <id>`), treat it like running untrusted code:
|
||||
- The install path is the per-plugin directory under the active plugin install root.
|
||||
- OpenClaw runs a built-in dangerous-code scan before install/update. `critical` findings block by default.
|
||||
- OpenClaw uses `npm pack` and then runs `npm install --omit=dev` in that directory (npm lifecycle scripts can execute code during install).
|
||||
- OpenClaw uses `npm pack`, then runs a project-local `npm install --omit=dev --ignore-scripts` in that directory. Inherited global npm install settings are ignored so dependencies stay under the plugin install path.
|
||||
- Prefer pinned, exact versions (`@scope/pkg@1.2.3`), and inspect the unpacked code on disk before enabling.
|
||||
- `--dangerously-force-unsafe-install` is break-glass only for built-in scan false positives on plugin install/update flows. It does not bypass plugin `before_install` hook policy blocks and does not bypass scan failures.
|
||||
- Gateway-backed skill dependency installs follow the same dangerous/suspicious split: built-in `critical` findings block unless the caller explicitly sets `dangerouslyForceUnsafeInstall`, while suspicious findings still warn only. `openclaw skills install` remains the separate ClawHub skill download/install flow.
|
||||
|
||||
@@ -16,6 +16,10 @@ Tailscale provides HTTPS, routing, and (for Serve) identity headers.
|
||||
- `funnel`: Public HTTPS via `tailscale funnel`. OpenClaw requires a shared password.
|
||||
- `off`: Default (no Tailscale automation).
|
||||
|
||||
Status and audit output use **Tailscale exposure** for this OpenClaw Serve/Funnel
|
||||
mode. `off` means OpenClaw is not managing Serve or Funnel; it does not mean the
|
||||
local Tailscale daemon is stopped or logged out.
|
||||
|
||||
## Auth
|
||||
|
||||
Set `gateway.auth.mode` to control the handshake:
|
||||
|
||||
@@ -200,6 +200,12 @@ Use `error.details.code` from the failed `connect` response to pick the next act
|
||||
| `AUTH_DEVICE_TOKEN_MISMATCH` | Cached per-device token is stale or revoked. | Rotate/re-approve device token using [devices CLI](/cli/devices), then reconnect. |
|
||||
| `PAIRING_REQUIRED` | Device identity needs approval. Check `error.details.reason` for `not-paired`, `scope-upgrade`, `role-upgrade`, or `metadata-upgrade`, and use `requestId` / `remediationHint` when present. | Approve pending request: `openclaw devices list` then `openclaw devices approve <requestId>`. Scope/role upgrades use the same flow after you review the requested access. |
|
||||
|
||||
Direct loopback backend RPCs authenticated with the shared gateway
|
||||
token/password should not depend on the CLI's paired-device scope baseline. If
|
||||
subagents or other internal calls still fail with `scope-upgrade`, verify the
|
||||
caller is using `client.id: "gateway-client"` and `client.mode: "backend"` and
|
||||
is not forcing an explicit `deviceIdentity` or device token.
|
||||
|
||||
Device auth v2 migration check:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -595,10 +595,9 @@ and troubleshooting see the main [FAQ](/help/faq).
|
||||
<Accordion title="How does Codex auth work?">
|
||||
OpenClaw supports **OpenAI Code (Codex)** via OAuth (ChatGPT sign-in). Use
|
||||
`openai-codex/gpt-5.5` for Codex OAuth through the default PI runner. Use
|
||||
`openai/gpt-5.4` for current direct OpenAI API-key access. GPT-5.5 direct
|
||||
API-key access is supported once OpenAI enables it on the public API; today
|
||||
GPT-5.5 uses subscription/OAuth via `openai-codex/gpt-5.5` or native Codex
|
||||
app-server runs with `openai/gpt-5.5` and `embeddedHarness.runtime: "codex"`.
|
||||
`openai/gpt-5.5` for direct OpenAI API-key access. GPT-5.5 can also use
|
||||
subscription/OAuth via `openai-codex/gpt-5.5` or native Codex app-server
|
||||
runs with `openai/gpt-5.5` and `embeddedHarness.runtime: "codex"`.
|
||||
See [Model providers](/concepts/model-providers) and [Onboarding (CLI)](/start/wizard).
|
||||
</Accordion>
|
||||
|
||||
@@ -606,8 +605,7 @@ and troubleshooting see the main [FAQ](/help/faq).
|
||||
`openai-codex` is the provider and auth-profile id for ChatGPT/Codex OAuth.
|
||||
It is also the explicit PI model prefix for Codex OAuth:
|
||||
|
||||
- `openai/gpt-5.4` = current direct OpenAI API-key route in PI
|
||||
- `openai/gpt-5.5` = future direct API-key route once OpenAI enables GPT-5.5 on the API
|
||||
- `openai/gpt-5.5` = current direct OpenAI API-key route in PI
|
||||
- `openai-codex/gpt-5.5` = Codex OAuth route in PI
|
||||
- `openai/gpt-5.5` + `embeddedHarness.runtime: "codex"` = native Codex app-server route
|
||||
- `openai-codex:...` = auth profile id, not a model ref
|
||||
|
||||
@@ -21,7 +21,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
agents.defaults.model.primary
|
||||
```
|
||||
|
||||
Models are referenced as `provider/model` (example: `openai/gpt-5.4` or `openai-codex/gpt-5.5`). If you omit the provider, OpenClaw first tries an alias, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider as a deprecated compatibility path. 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. You should still **explicitly** set `provider/model`.
|
||||
Models are referenced as `provider/model` (example: `openai/gpt-5.5` or `openai-codex/gpt-5.5`). If you omit the provider, OpenClaw first tries an alias, then a unique configured-provider match for that exact model id, and only then falls back to the configured default provider as a deprecated compatibility path. 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. You should still **explicitly** set `provider/model`.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -146,13 +146,10 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
<Accordion title="Can I use GPT 5.5 for daily tasks and Codex 5.5 for coding?">
|
||||
Yes. Set one as default and switch as needed:
|
||||
|
||||
- **Quick switch (per session):** `/model openai/gpt-5.4` for current direct OpenAI API-key tasks or `/model openai-codex/gpt-5.5` for GPT-5.5 Codex OAuth tasks.
|
||||
- **Default:** set `agents.defaults.model.primary` to `openai/gpt-5.4` for API-key usage or `openai-codex/gpt-5.5` for GPT-5.5 Codex OAuth usage.
|
||||
- **Quick switch (per session):** `/model openai/gpt-5.5` for current direct OpenAI API-key tasks or `/model openai-codex/gpt-5.5` for GPT-5.5 Codex OAuth tasks.
|
||||
- **Default:** set `agents.defaults.model.primary` to `openai/gpt-5.5` for API-key usage or `openai-codex/gpt-5.5` for GPT-5.5 Codex OAuth usage.
|
||||
- **Sub-agents:** route coding tasks to sub-agents with a different default model.
|
||||
|
||||
Direct API-key access for `openai/gpt-5.5` is supported once OpenAI enables
|
||||
GPT-5.5 on the public API. Until then GPT-5.5 is subscription/OAuth-only.
|
||||
|
||||
See [Models](/concepts/models) and [Slash commands](/tools/slash-commands).
|
||||
|
||||
</Accordion>
|
||||
@@ -160,8 +157,8 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
<Accordion title="How do I configure fast mode for GPT 5.5?">
|
||||
Use either a session toggle or a config default:
|
||||
|
||||
- **Per session:** send `/fast on` while the session is using `openai/gpt-5.4` or `openai-codex/gpt-5.5`.
|
||||
- **Per model default:** set `agents.defaults.models["openai/gpt-5.4"].params.fastMode` or `agents.defaults.models["openai-codex/gpt-5.5"].params.fastMode` to `true`.
|
||||
- **Per session:** send `/fast on` while the session is using `openai/gpt-5.5` or `openai-codex/gpt-5.5`.
|
||||
- **Per model default:** set `agents.defaults.models["openai/gpt-5.5"].params.fastMode` or `agents.defaults.models["openai-codex/gpt-5.5"].params.fastMode` to `true`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -170,7 +167,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openai/gpt-5.4": {
|
||||
"openai/gpt-5.5": {
|
||||
params: {
|
||||
fastMode: true,
|
||||
},
|
||||
@@ -241,7 +238,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
model: { primary: "minimax/MiniMax-M2.7" },
|
||||
models: {
|
||||
"minimax/MiniMax-M2.7": { alias: "minimax" },
|
||||
"openai/gpt-5.4": { alias: "gpt" },
|
||||
"openai/gpt-5.5": { alias: "gpt" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -269,7 +266,7 @@ troubleshooting, see the main [FAQ](/help/faq).
|
||||
|
||||
- `opus` → `anthropic/claude-opus-4-6`
|
||||
- `sonnet` → `anthropic/claude-sonnet-4-6`
|
||||
- `gpt` → `openai/gpt-5.4` for API-key setups, or `openai-codex/gpt-5.5` when configured for Codex OAuth
|
||||
- `gpt` → `openai/gpt-5.5` for API-key setups, or `openai-codex/gpt-5.5` when configured for Codex OAuth
|
||||
- `gpt-mini` → `openai/gpt-5.4-mini`
|
||||
- `gpt-nano` → `openai/gpt-5.4-nano`
|
||||
- `gemini` → `google/gemini-3.1-pro-preview`
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
summary: "How to review the GPT-5.4 / Codex parity program as four merge units"
|
||||
title: "GPT-5.4 / Codex parity maintainer notes"
|
||||
summary: "How to review the GPT-5.5 / Codex parity program as four merge units"
|
||||
title: "GPT-5.5 / Codex parity maintainer notes"
|
||||
read_when:
|
||||
- Reviewing the GPT-5.4 / Codex parity PR series
|
||||
- Reviewing the GPT-5.5 / Codex parity PR series
|
||||
- Maintaining the six-contract agentic architecture behind the parity program
|
||||
---
|
||||
|
||||
This note explains how to review the GPT-5.4 / Codex parity program as four merge units without losing the original six-contract architecture.
|
||||
This note explains how to review the GPT-5.5 / Codex parity program as four merge units without losing the original six-contract architecture.
|
||||
|
||||
## Merge units
|
||||
|
||||
@@ -59,7 +59,7 @@ Does not own:
|
||||
|
||||
Owns:
|
||||
|
||||
- first-wave GPT-5.4 vs Opus 4.6 scenario pack
|
||||
- first-wave GPT-5.5 vs Opus 4.6 scenario pack
|
||||
- parity documentation
|
||||
- parity report and release-gate mechanics
|
||||
|
||||
@@ -123,7 +123,7 @@ Expected artifacts from PR D:
|
||||
|
||||
## Release gate
|
||||
|
||||
Do not claim GPT-5.4 parity or superiority over Opus 4.6 until:
|
||||
Do not claim GPT-5.5 parity or superiority over Opus 4.6 until:
|
||||
|
||||
- PR A, PR B, and PR C are merged
|
||||
- PR D runs the first-wave parity pack cleanly
|
||||
@@ -132,7 +132,7 @@ Do not claim GPT-5.4 parity or superiority over Opus 4.6 until:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["PR A-C merged"] --> B["Run GPT-5.4 parity pack"]
|
||||
A["PR A-C merged"] --> B["Run GPT-5.5 parity pack"]
|
||||
A --> C["Run Opus 4.6 parity pack"]
|
||||
B --> D["qa-suite-summary.json"]
|
||||
C --> E["qa-suite-summary.json"]
|
||||
@@ -146,7 +146,7 @@ flowchart LR
|
||||
|
||||
The parity harness is not the only evidence source. Keep this split explicit in review:
|
||||
|
||||
- PR D owns the scenario-based GPT-5.4 vs Opus 4.6 comparison
|
||||
- PR D owns the scenario-based GPT-5.5 vs Opus 4.6 comparison
|
||||
- PR B deterministic suites still own auth/proxy/DNS and full-access truthfulness evidence
|
||||
|
||||
## Quick maintainer merge workflow
|
||||
@@ -179,13 +179,13 @@ If any one of the evidence bar items is missing, request changes instead of merg
|
||||
| No fake progress or fake tool completion | PR A + PR D | parity fake-success count plus scenario-level report details |
|
||||
| No false `/elevated full` guidance | PR B | deterministic runtime-truthfulness suites |
|
||||
| Replay/liveness failures remain explicit | PR C + PR D | lifecycle/replay suites plus `compaction-retry-mutating-tool` |
|
||||
| GPT-5.4 matches or beats Opus 4.6 | PR D | `qa-agentic-parity-report.md` and `qa-agentic-parity-summary.json` |
|
||||
| GPT-5.5 matches or beats Opus 4.6 | PR D | `qa-agentic-parity-report.md` and `qa-agentic-parity-summary.json` |
|
||||
|
||||
## Reviewer shorthand: before vs after
|
||||
|
||||
| User-visible problem before | Review signal after |
|
||||
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| GPT-5.4 stopped after planning | PR A shows act-or-block behavior instead of commentary-only completion |
|
||||
| GPT-5.5 stopped after planning | PR A shows act-or-block behavior instead of commentary-only completion |
|
||||
| Tool use felt brittle with strict OpenAI/Codex schemas | PR C keeps tool registration and parameter-free invocation predictable |
|
||||
| `/elevated full` hints were sometimes misleading | PR B ties guidance to actual runtime capability and blocked reasons |
|
||||
| Long tasks could disappear into replay/compaction ambiguity | PR C emits explicit paused, blocked, abandoned, and replay-invalid state |
|
||||
@@ -193,4 +193,4 @@ If any one of the evidence bar items is missing, request changes instead of merg
|
||||
|
||||
## Related
|
||||
|
||||
- [GPT-5.4 / Codex agentic parity](/help/gpt54-codex-agentic-parity)
|
||||
- [GPT-5.5 / Codex agentic parity](/help/gpt55-codex-agentic-parity)
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
summary: "How OpenClaw closes agentic execution gaps for GPT-5.4 and Codex-style models"
|
||||
title: "GPT-5.4 / Codex agentic parity"
|
||||
summary: "How OpenClaw closes agentic execution gaps for GPT-5.5 and Codex-style models"
|
||||
title: "GPT-5.5 / Codex agentic parity"
|
||||
read_when:
|
||||
- Debugging GPT-5.4 or Codex agent behavior
|
||||
- Debugging GPT-5.5 or Codex agent behavior
|
||||
- Comparing OpenClaw agentic behavior across frontier models
|
||||
- Reviewing the strict-agentic, tool-schema, elevation, and replay fixes
|
||||
---
|
||||
|
||||
# GPT-5.4 / Codex Agentic Parity in OpenClaw
|
||||
# GPT-5.5 / Codex Agentic Parity in OpenClaw
|
||||
|
||||
OpenClaw already worked well with tool-using frontier models, but GPT-5.4 and Codex-style models were still underperforming in a few practical ways:
|
||||
OpenClaw already worked well with tool-using frontier models, but GPT-5.5 and Codex-style models were still underperforming in a few practical ways:
|
||||
|
||||
- they could stop after planning instead of doing the work
|
||||
- they could use strict OpenAI/Codex tool schemas incorrectly
|
||||
@@ -27,7 +27,7 @@ This slice adds an opt-in `strict-agentic` execution contract for embedded Pi GP
|
||||
|
||||
When enabled, OpenClaw stops accepting plan-only turns as “good enough” completion. If the model only says what it intends to do and does not actually use tools or make progress, OpenClaw retries with an act-now steer and then fails closed with an explicit blocked state instead of silently ending the task.
|
||||
|
||||
This improves the GPT-5.4 experience most on:
|
||||
This improves the GPT-5.5 experience most on:
|
||||
|
||||
- short “ok do it” follow-ups
|
||||
- code tasks where the first step is obvious
|
||||
@@ -40,7 +40,7 @@ This slice makes OpenClaw tell the truth about two things:
|
||||
- why the provider/runtime call failed
|
||||
- whether `/elevated full` is actually available
|
||||
|
||||
That means GPT-5.4 gets better runtime signals for missing scope, auth refresh failures, HTML 403 auth failures, proxy issues, DNS or timeout failures, and blocked full-access modes. The model is less likely to hallucinate the wrong remediation or keep asking for a permission mode the runtime cannot provide.
|
||||
That means GPT-5.5 gets better runtime signals for missing scope, auth refresh failures, HTML 403 auth failures, proxy issues, DNS or timeout failures, and blocked full-access modes. The model is less likely to hallucinate the wrong remediation or keep asking for a permission mode the runtime cannot provide.
|
||||
|
||||
### PR C: execution correctness
|
||||
|
||||
@@ -53,7 +53,7 @@ The tool-compat work reduces schema friction for strict OpenAI/Codex tool regist
|
||||
|
||||
### PR D: parity harness
|
||||
|
||||
This slice adds the first-wave QA-lab parity pack so GPT-5.4 and Opus 4.6 can be exercised through the same scenarios and compared using shared evidence.
|
||||
This slice adds the first-wave QA-lab parity pack so GPT-5.5 and Opus 4.6 can be exercised through the same scenarios and compared using shared evidence.
|
||||
|
||||
The parity pack is the proof layer. It does not change runtime behavior by itself.
|
||||
|
||||
@@ -62,7 +62,7 @@ After you have two `qa-suite-summary.json` artifacts, generate the release-gate
|
||||
```bash
|
||||
pnpm openclaw qa parity-report \
|
||||
--repo-root . \
|
||||
--candidate-summary .artifacts/qa-e2e/gpt54/qa-suite-summary.json \
|
||||
--candidate-summary .artifacts/qa-e2e/gpt55/qa-suite-summary.json \
|
||||
--baseline-summary .artifacts/qa-e2e/opus46/qa-suite-summary.json \
|
||||
--output-dir .artifacts/qa-e2e/parity
|
||||
```
|
||||
@@ -73,16 +73,16 @@ That command writes:
|
||||
- a machine-readable JSON verdict
|
||||
- an explicit `pass` / `fail` gate result
|
||||
|
||||
## Why this improves GPT-5.4 in practice
|
||||
## Why this improves GPT-5.5 in practice
|
||||
|
||||
Before this work, GPT-5.4 on OpenClaw could feel less agentic than Opus in real coding sessions because the runtime tolerated behaviors that are especially harmful for GPT-5-style models:
|
||||
Before this work, GPT-5.5 on OpenClaw could feel less agentic than Opus in real coding sessions because the runtime tolerated behaviors that are especially harmful for GPT-5-style models:
|
||||
|
||||
- commentary-only turns
|
||||
- schema friction around tools
|
||||
- vague permission feedback
|
||||
- silent replay or compaction breakage
|
||||
|
||||
The goal is not to make GPT-5.4 imitate Opus. The goal is to give GPT-5.4 a runtime contract that rewards real progress, supplies cleaner tool and permission semantics, and turns failure modes into explicit machine- and human-readable states.
|
||||
The goal is not to make GPT-5.5 imitate Opus. The goal is to give GPT-5.5 a runtime contract that rewards real progress, supplies cleaner tool and permission semantics, and turns failure modes into explicit machine- and human-readable states.
|
||||
|
||||
That changes the user experience from:
|
||||
|
||||
@@ -92,15 +92,15 @@ to:
|
||||
|
||||
- “the model either acted, or OpenClaw surfaced the exact reason it could not”
|
||||
|
||||
## Before vs after for GPT-5.4 users
|
||||
## Before vs after for GPT-5.5 users
|
||||
|
||||
| Before this program | After PR A-D |
|
||||
| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| GPT-5.4 could stop after a reasonable plan without taking the next tool step | PR A turns “plan only” into “act now or surface a blocked state” |
|
||||
| GPT-5.5 could stop after a reasonable plan without taking the next tool step | PR A turns “plan only” into “act now or surface a blocked state” |
|
||||
| Strict tool schemas could reject parameter-free or OpenAI/Codex-shaped tools in confusing ways | PR C makes provider-owned tool registration and invocation more predictable |
|
||||
| `/elevated full` guidance could be vague or wrong in blocked runtimes | PR B gives GPT-5.4 and the user truthful runtime and permission hints |
|
||||
| `/elevated full` guidance could be vague or wrong in blocked runtimes | PR B gives GPT-5.5 and the user truthful runtime and permission hints |
|
||||
| Replay or compaction failures could feel like the task silently disappeared | PR C surfaces paused, blocked, abandoned, and replay-invalid outcomes explicitly |
|
||||
| “GPT-5.4 feels worse than Opus” was mostly anecdotal | PR D turns that into the same scenario pack, the same metrics, and a hard pass/fail gate |
|
||||
| “GPT-5.5 feels worse than Opus” was mostly anecdotal | PR D turns that into the same scenario pack, the same metrics, and a hard pass/fail gate |
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -123,7 +123,7 @@ flowchart TD
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Merged runtime slices (PR A-C)"] --> B["Run GPT-5.4 parity pack"]
|
||||
A["Merged runtime slices (PR A-C)"] --> B["Run GPT-5.5 parity pack"]
|
||||
A --> C["Run Opus 4.6 parity pack"]
|
||||
B --> D["qa-suite-summary.json"]
|
||||
C --> E["qa-suite-summary.json"]
|
||||
@@ -162,7 +162,7 @@ Checks that a task with a real mutating write keeps replay-unsafety explicit ins
|
||||
|
||||
## Scenario matrix
|
||||
|
||||
| Scenario | What it tests | Good GPT-5.4 behavior | Failure signal |
|
||||
| Scenario | What it tests | Good GPT-5.5 behavior | Failure signal |
|
||||
| ---------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
|
||||
| `approval-turn-tool-followthrough` | Short approval turns after a plan | Starts the first concrete tool action immediately instead of restating intent | plan-only follow-up, no tool activity, or blocked turn without a real blocker |
|
||||
| `model-switch-tool-continuity` | Runtime/model switching under tool use | Preserves task context and continues acting coherently | resets into commentary, loses tool context, or stops after switch |
|
||||
@@ -172,7 +172,7 @@ Checks that a task with a real mutating write keeps replay-unsafety explicit ins
|
||||
|
||||
## Release gate
|
||||
|
||||
GPT-5.4 can only be considered at parity or better when the merged runtime passes the parity pack and the runtime-truthfulness regressions at the same time.
|
||||
GPT-5.5 can only be considered at parity or better when the merged runtime passes the parity pack and the runtime-truthfulness regressions at the same time.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
@@ -191,24 +191,24 @@ For the first-wave harness, the gate compares:
|
||||
|
||||
Parity evidence is intentionally split across two layers:
|
||||
|
||||
- PR D proves same-scenario GPT-5.4 vs Opus 4.6 behavior with QA-lab
|
||||
- PR D proves same-scenario GPT-5.5 vs Opus 4.6 behavior with QA-lab
|
||||
- PR B deterministic suites prove auth, proxy, DNS, and `/elevated full` truthfulness outside the harness
|
||||
|
||||
## Goal-to-evidence matrix
|
||||
|
||||
| Completion gate item | Owning PR | Evidence source | Pass signal |
|
||||
| -------------------------------------------------------- | ----------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
|
||||
| GPT-5.4 no longer stalls after planning | PR A | `approval-turn-tool-followthrough` plus PR A runtime suites | approval turns trigger real work or an explicit blocked state |
|
||||
| GPT-5.4 no longer fakes progress or fake tool completion | PR A + PR D | parity report scenario outcomes and fake-success count | no suspicious pass results and no commentary-only completion |
|
||||
| GPT-5.4 no longer gives false `/elevated full` guidance | PR B | deterministic truthfulness suites | blocked reasons and full-access hints stay runtime-accurate |
|
||||
| GPT-5.5 no longer stalls after planning | PR A | `approval-turn-tool-followthrough` plus PR A runtime suites | approval turns trigger real work or an explicit blocked state |
|
||||
| GPT-5.5 no longer fakes progress or fake tool completion | PR A + PR D | parity report scenario outcomes and fake-success count | no suspicious pass results and no commentary-only completion |
|
||||
| GPT-5.5 no longer gives false `/elevated full` guidance | PR B | deterministic truthfulness suites | blocked reasons and full-access hints stay runtime-accurate |
|
||||
| Replay/liveness failures stay explicit | PR C + PR D | PR C lifecycle/replay suites plus `compaction-retry-mutating-tool` | mutating work keeps replay-unsafety explicit instead of silently disappearing |
|
||||
| GPT-5.4 matches or beats Opus 4.6 on the agreed metrics | PR D | `qa-agentic-parity-report.md` and `qa-agentic-parity-summary.json` | same scenario coverage and no regression on completion, stop behavior, or valid tool use |
|
||||
| GPT-5.5 matches or beats Opus 4.6 on the agreed metrics | PR D | `qa-agentic-parity-report.md` and `qa-agentic-parity-summary.json` | same scenario coverage and no regression on completion, stop behavior, or valid tool use |
|
||||
|
||||
## How to read the parity verdict
|
||||
|
||||
Use the verdict in `qa-agentic-parity-summary.json` as the final machine-readable decision for the first-wave parity pack.
|
||||
|
||||
- `pass` means GPT-5.4 covered the same scenarios as Opus 4.6 and did not regress on the agreed aggregate metrics.
|
||||
- `pass` means GPT-5.5 covered the same scenarios as Opus 4.6 and did not regress on the agreed aggregate metrics.
|
||||
- `fail` means at least one hard gate tripped: weaker completion, worse unintended stops, weaker valid tool use, any fake-success case, or mismatched scenario coverage.
|
||||
- “shared/base CI issue” is not itself a parity result. If CI noise outside PR D blocks a run, the verdict should wait for a clean merged-runtime execution instead of being inferred from branch-era logs.
|
||||
- Auth, proxy, DNS, and `/elevated full` truthfulness still come from PR B’s deterministic suites, so the final release claim needs both: a passing PR D parity verdict and green PR B truthfulness coverage.
|
||||
@@ -218,7 +218,7 @@ Use the verdict in `qa-agentic-parity-summary.json` as the final machine-readabl
|
||||
Use `strict-agentic` when:
|
||||
|
||||
- the agent is expected to act immediately when a next step is obvious
|
||||
- GPT-5.4 or Codex-family models are the primary runtime
|
||||
- GPT-5.5 or Codex-family models are the primary runtime
|
||||
- you prefer explicit blocked states over “helpful” recap-only replies
|
||||
|
||||
Keep the default contract when:
|
||||
@@ -229,4 +229,4 @@ Keep the default contract when:
|
||||
|
||||
## Related
|
||||
|
||||
- [GPT-5.4 / Codex parity maintainer notes](/help/gpt54-codex-agentic-parity-maintainers)
|
||||
- [GPT-5.5 / Codex parity maintainer notes](/help/gpt55-codex-agentic-parity-maintainers)
|
||||
@@ -164,6 +164,18 @@ OPENCLAW_LIVE_CLI_BACKEND=1 \
|
||||
pnpm test:live src/gateway/gateway-cli-backend.live.test.ts
|
||||
```
|
||||
|
||||
Cheap Gemini MCP config smoke:
|
||||
|
||||
```bash
|
||||
OPENCLAW_LIVE_TEST=1 \
|
||||
pnpm test:live src/agents/cli-runner/bundle-mcp.gemini.live.test.ts
|
||||
```
|
||||
|
||||
This does not ask Gemini to generate a response. It writes the same system
|
||||
settings OpenClaw gives Gemini, then runs `gemini --debug mcp list` to prove a
|
||||
saved `transport: "streamable-http"` server is normalized to Gemini's HTTP MCP
|
||||
shape and can connect to a local streamable-HTTP MCP server.
|
||||
|
||||
Docker recipe:
|
||||
|
||||
```bash
|
||||
@@ -207,6 +219,7 @@ Notes:
|
||||
- Overrides:
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=claude`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=codex`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=droid`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=gemini`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENT=opencode`
|
||||
- `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude,codex,gemini`
|
||||
@@ -238,6 +251,7 @@ Single-agent Docker recipes:
|
||||
```bash
|
||||
pnpm test:docker:live-acp-bind:claude
|
||||
pnpm test:docker:live-acp-bind:codex
|
||||
pnpm test:docker:live-acp-bind:droid
|
||||
pnpm test:docker:live-acp-bind:gemini
|
||||
pnpm test:docker:live-acp-bind:opencode
|
||||
```
|
||||
@@ -246,8 +260,9 @@ Docker notes:
|
||||
|
||||
- The Docker runner lives at `scripts/test-live-acp-bind-docker.sh`.
|
||||
- By default, it runs the ACP bind smoke against the aggregate live CLI agents in sequence: `claude`, `codex`, then `gemini`.
|
||||
- Use `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=codex`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=gemini`, or `OPENCLAW_LIVE_ACP_BIND_AGENTS=opencode` to narrow the matrix.
|
||||
- It sources `~/.profile`, stages the matching CLI auth material into the container, then installs the requested live CLI (`@anthropic-ai/claude-code`, `@openai/codex`, `@google/gemini-cli`, or `opencode-ai`) if missing. The ACP backend itself is the bundled embedded `acpx/runtime` package from the `acpx` plugin.
|
||||
- Use `OPENCLAW_LIVE_ACP_BIND_AGENTS=claude`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=codex`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=droid`, `OPENCLAW_LIVE_ACP_BIND_AGENTS=gemini`, or `OPENCLAW_LIVE_ACP_BIND_AGENTS=opencode` to narrow the matrix.
|
||||
- It sources `~/.profile`, stages the matching CLI auth material into the container, then installs the requested live CLI (`@anthropic-ai/claude-code`, `@openai/codex`, Factory Droid via `https://app.factory.ai/cli`, `@google/gemini-cli`, or `opencode-ai`) if missing. The ACP backend itself is the bundled embedded `acpx/runtime` package from the `acpx` plugin.
|
||||
- The Droid Docker variant stages `~/.factory` for settings, forwards `FACTORY_API_KEY`, and requires that API key because local Factory OAuth/keyring auth is not portable into the container. It uses ACPX's built-in `droid exec --output-format acp` registry entry.
|
||||
- The OpenCode Docker variant is a strict single-agent regression lane. It writes a temporary `OPENCODE_CONFIG_CONTENT` default model from `OPENCLAW_LIVE_ACP_BIND_OPENCODE_MODEL` (default `opencode/kimi-k2.6`) after sourcing `~/.profile`, and `pnpm test:docker:live-acp-bind:opencode` requires a bound assistant transcript instead of accepting the generic post-bind skip.
|
||||
- Direct `acpx` CLI calls are only a manual/workaround path for comparing behavior outside the Gateway. The Docker ACP bind smoke exercises OpenClaw's embedded `acpx` runtime backend.
|
||||
|
||||
|
||||
@@ -120,6 +120,12 @@ runs the same lanes before release approval.
|
||||
endpoint.
|
||||
- Use `OPENCLAW_NPM_ONBOARD_CHANNEL=discord` to run the same packaged-install
|
||||
lane with Discord.
|
||||
- `pnpm test:docker:session-runtime-context`
|
||||
- Runs a deterministic built-app Docker smoke for embedded runtime context
|
||||
transcripts. It verifies hidden OpenClaw runtime context is persisted as a
|
||||
non-display custom message instead of leaking into the visible user turn,
|
||||
then seeds an affected broken session JSONL and verifies
|
||||
`openclaw doctor --fix` rewrites it to the active branch with a backup.
|
||||
- `pnpm test:docker:npm-telegram-live`
|
||||
- Installs a published OpenClaw package in Docker, runs installed-package
|
||||
onboarding, configures Telegram through the installed CLI, then reuses the
|
||||
@@ -393,10 +399,11 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
||||
- `pnpm test --watch` still uses the native root `vitest.config.ts` project graph, because a multi-shard watch loop is not practical.
|
||||
- `pnpm test`, `pnpm test:watch`, and `pnpm test:perf:imports` route explicit file/directory targets through scoped lanes first, so `pnpm test extensions/discord/src/monitor/message-handler.preflight.test.ts` avoids paying the full root project startup tax.
|
||||
- `pnpm test:changed` expands changed git paths into the same scoped lanes when the diff only touches routable source/test files; config/setup edits still fall back to the broad root-project rerun.
|
||||
- `pnpm check:changed` is the normal smart local gate for narrow work. It classifies the diff into core, core tests, extensions, extension tests, apps, docs, release metadata, and tooling, then runs the matching typecheck/lint/test lanes. Public Plugin SDK and plugin-contract changes include one extension validation pass because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks instead of the full suite, with a guard that rejects package changes outside the top-level version field.
|
||||
- `pnpm check:changed` is the normal smart local gate for narrow work. It classifies the diff into core, core tests, extensions, extension tests, apps, docs, release metadata, live Docker tooling, and tooling, then runs the matching typecheck/lint/test lanes. Public Plugin SDK and plugin-contract changes include one extension validation pass because extensions depend on those core contracts. Release metadata-only version bumps run targeted version/config/root-dependency checks instead of the full suite, with a guard that rejects package changes outside the top-level version field.
|
||||
- Live Docker ACP harness edits run a focused local gate: shell syntax for the live Docker auth scripts, live Docker scheduler dry-run, ACP bind unit tests, and the ACPX extension tests. `package.json` changes are included only when the diff is limited to `scripts["test:docker:live-*"]`; dependency, export, version, and other package-surface edits still use the broader guards.
|
||||
- Import-light unit tests from agents, commands, plugins, auto-reply helpers, `plugin-sdk`, and similar pure utility areas route through the `unit-fast` lane, which skips `test/setup-openclaw-runtime.ts`; stateful/runtime-heavy files stay on the existing lanes.
|
||||
- Selected `plugin-sdk` and `commands` helper source files also map changed-mode runs to explicit sibling tests in those light lanes, so helper edits avoid rerunning the full heavy suite for that directory.
|
||||
- `auto-reply` has three dedicated buckets: top-level core helpers, top-level `reply.*` integration tests, and the `src/auto-reply/reply/**` subtree. This keeps the heaviest reply harness work off the cheap status/chunk/token tests.
|
||||
- `auto-reply` has dedicated buckets for top-level core helpers, top-level `reply.*` integration tests, and the `src/auto-reply/reply/**` subtree. CI further splits the reply subtree into agent-runner, dispatch, and commands/state-routing shards so one import-heavy bucket does not own the full Node tail.
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -462,6 +469,10 @@ Think of the suites as “increasing realism” (and increasing flakiness/cost):
|
||||
import-breakdown output.
|
||||
- `pnpm test:perf:imports:changed` scopes the same profiling view to
|
||||
files changed since `origin/main`.
|
||||
- Shard timing data is written to `.artifacts/vitest-shard-timings.json`.
|
||||
Whole-config runs use the config path as the key; include-pattern CI
|
||||
shards append the shard name so filtered shards can be tracked
|
||||
separately.
|
||||
- When one hot test still spends most of its time in startup imports,
|
||||
keep heavy dependencies behind a narrow local `*.runtime.ts` seam and
|
||||
mock that seam directly instead of deep-importing runtime helpers just
|
||||
@@ -583,18 +594,19 @@ These Docker runners split into two buckets:
|
||||
`OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you
|
||||
explicitly want the larger exhaustive scan.
|
||||
- `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the live Docker lanes. It also builds one shared `scripts/e2e/Dockerfile` image via `test:docker:e2e-build` and reuses it for the E2E container smoke runners that exercise the built app. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
|
||||
- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths.
|
||||
|
||||
The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store:
|
||||
|
||||
- Direct models: `pnpm test:docker:live-models` (script: `scripts/test-live-models-docker.sh`)
|
||||
- ACP bind smoke: `pnpm test:docker:live-acp-bind` (script: `scripts/test-live-acp-bind-docker.sh`; covers Claude, Codex, and Gemini by default, with strict OpenCode coverage via `pnpm test:docker:live-acp-bind:opencode`)
|
||||
- ACP bind smoke: `pnpm test:docker:live-acp-bind` (script: `scripts/test-live-acp-bind-docker.sh`; covers Claude, Codex, and Gemini by default, with strict Droid/OpenCode coverage via `pnpm test:docker:live-acp-bind:droid` and `pnpm test:docker:live-acp-bind:opencode`)
|
||||
- CLI backend smoke: `pnpm test:docker:live-cli-backend` (script: `scripts/test-live-cli-backend-docker.sh`)
|
||||
- Codex app-server harness smoke: `pnpm test:docker:live-codex-harness` (script: `scripts/test-live-codex-harness-docker.sh`)
|
||||
- Gateway + dev agent: `pnpm test:docker:live-gateway` (script: `scripts/test-live-gateway-models-docker.sh`)
|
||||
- Open WebUI live smoke: `pnpm test:docker:openwebui` (script: `scripts/e2e/openwebui-docker.sh`)
|
||||
- Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`)
|
||||
- Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies doctor repairs activated plugin runtime deps, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`.
|
||||
- Session runtime context smoke: `pnpm test:docker:session-runtime-context` verifies hidden runtime context transcript persistence plus doctor repair of affected duplicated prompt-rewrite branches.
|
||||
- Bun global install smoke: `bash scripts/e2e/bun-global-install-smoke.sh` packs the current tree, installs it with `bun install -g` in an isolated home, and verifies `openclaw infer image providers --json` returns bundled image providers instead of hanging. Reuse a prebuilt tarball with `OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host build with `OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0`, or copy `dist/` from a built Docker image with `OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE=openclaw-dockerfile-smoke:local`.
|
||||
- Installer Docker smoke: `bash scripts/test-install-sh-docker.sh` shares one npm cache across its root, update, and direct-npm containers. Update smoke defaults to npm `latest` as the stable baseline before upgrading to the candidate tarball. Non-root installer checks keep an isolated npm cache so root-owned cache entries do not mask user-local install behavior. Set `OPENCLAW_INSTALL_SMOKE_NPM_CACHE_DIR=/path/to/cache` to reuse the root/update/direct-npm cache across local reruns.
|
||||
- Install Smoke CI skips the duplicate direct-npm global update with `OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL=1`; run the script locally without that env when direct `npm install -g` coverage is needed.
|
||||
|
||||
@@ -193,7 +193,14 @@ read_when:
|
||||
},
|
||||
"gateway": {
|
||||
"mode": "local",
|
||||
"bind": "auto"
|
||||
"bind": "auto",
|
||||
"controlUi": {
|
||||
"allowedOrigins": [
|
||||
"https://my-openclaw.fly.dev",
|
||||
"http://localhost:3000",
|
||||
"http://127.0.0.1:3000"
|
||||
]
|
||||
}
|
||||
},
|
||||
"meta": {}
|
||||
}
|
||||
@@ -202,6 +209,12 @@ read_when:
|
||||
|
||||
**Note:** With `OPENCLAW_STATE_DIR=/data`, the config path is `/data/openclaw.json`.
|
||||
|
||||
**Note:** Replace `https://my-openclaw.fly.dev` with your real Fly app
|
||||
origin. Gateway startup seeds local Control UI origins from the runtime
|
||||
`--bind` and `--port` values so first boot can proceed before config exists,
|
||||
but browser access through Fly still needs the exact HTTPS origin listed in
|
||||
`gateway.controlUi.allowedOrigins`.
|
||||
|
||||
**Note:** The Discord token can come from either:
|
||||
|
||||
- Environment variable: `DISCORD_BOT_TOKEN` (recommended for secrets)
|
||||
|
||||
@@ -93,16 +93,20 @@ If your old installation had local-only encrypted history that was never backed
|
||||
openclaw matrix verify backup status
|
||||
```
|
||||
|
||||
5. If OpenClaw tells you a recovery key is needed, run:
|
||||
5. Put the recovery key for the Matrix account you are repairing in an account-specific environment variable. For a single default account, `MATRIX_RECOVERY_KEY` is fine. For multiple accounts, use one variable per account, for example `MATRIX_RECOVERY_KEY_ASSISTANT`, and add `--account assistant` to the command.
|
||||
|
||||
6. If OpenClaw tells you a recovery key is needed, run the command for the matching account:
|
||||
|
||||
```bash
|
||||
openclaw matrix verify backup restore --recovery-key "<your-recovery-key>"
|
||||
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin
|
||||
printf '%s\n' "$MATRIX_RECOVERY_KEY_ASSISTANT" | openclaw matrix verify backup restore --recovery-key-stdin --account assistant
|
||||
```
|
||||
|
||||
6. If this device is still unverified, run:
|
||||
7. If this device is still unverified, run the command for the matching account:
|
||||
|
||||
```bash
|
||||
openclaw matrix verify device "<your-recovery-key>"
|
||||
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin
|
||||
printf '%s\n' "$MATRIX_RECOVERY_KEY_ASSISTANT" | openclaw matrix verify device --recovery-key-stdin --account assistant
|
||||
```
|
||||
|
||||
If the recovery key is accepted and backup is usable, but `Cross-signing verified`
|
||||
@@ -116,13 +120,13 @@ If your old installation had local-only encrypted history that was never backed
|
||||
and type `yes` only when they match. The command exits successfully only
|
||||
after `Cross-signing verified` becomes `yes`.
|
||||
|
||||
7. If you are intentionally abandoning unrecoverable old history and want a fresh backup baseline for future messages, run:
|
||||
8. If you are intentionally abandoning unrecoverable old history and want a fresh backup baseline for future messages, run:
|
||||
|
||||
```bash
|
||||
openclaw matrix verify backup reset --yes
|
||||
```
|
||||
|
||||
8. If no server-side key backup exists yet, create one for future recoveries:
|
||||
9. If no server-side key backup exists yet, create one for future recoveries:
|
||||
|
||||
```bash
|
||||
openclaw matrix verify bootstrap
|
||||
@@ -242,15 +246,15 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins
|
||||
- Meaning: some old room keys existed only in the old local store and had never been uploaded to Matrix backup.
|
||||
- What to do: expect some old encrypted history to remain unavailable unless you can recover those keys manually from another verified client.
|
||||
|
||||
`Legacy Matrix encrypted state for account "..." has backed-up room keys, but no local backup decryption key was found. Ask the operator to run "openclaw matrix verify backup restore --recovery-key <key>" after upgrade if they have the recovery key.`
|
||||
`Legacy Matrix encrypted state for account "..." has backed-up room keys, but no local backup decryption key was found. Ask the operator to run "openclaw matrix verify backup restore --recovery-key-stdin" after upgrade if they have the recovery key.`
|
||||
|
||||
- Meaning: backup exists, but OpenClaw could not recover the recovery key automatically.
|
||||
- What to do: run `openclaw matrix verify backup restore --recovery-key "<your-recovery-key>"`.
|
||||
- What to do: run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin`.
|
||||
|
||||
`Failed inspecting legacy Matrix encrypted state for account "..." (...): ...`
|
||||
|
||||
- Meaning: OpenClaw found the old encrypted store, but it could not inspect it safely enough to prepare recovery.
|
||||
- What to do: rerun `openclaw doctor --fix`. If it repeats, keep the old state directory intact and recover using another verified Matrix client plus `openclaw matrix verify backup restore --recovery-key "<your-recovery-key>"`.
|
||||
- What to do: rerun `openclaw doctor --fix`. If it repeats, keep the old state directory intact and recover using another verified Matrix client plus `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin`.
|
||||
|
||||
`Legacy Matrix backup key was found for account "...", but .../recovery-key.json already contains a different recovery key. Leaving the existing file unchanged.`
|
||||
|
||||
@@ -265,39 +269,39 @@ If the old store reports room keys that were never backed up, OpenClaw warns ins
|
||||
`matrix: failed restoring room keys from legacy encrypted-state backup: ...`
|
||||
|
||||
- Meaning: the new plugin attempted restore but Matrix returned an error.
|
||||
- What to do: run `openclaw matrix verify backup status`, then retry with `openclaw matrix verify backup restore --recovery-key "<your-recovery-key>"` if needed.
|
||||
- What to do: run `openclaw matrix verify backup status`, then retry with `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin` if needed.
|
||||
|
||||
### Manual recovery messages
|
||||
|
||||
`Backup key is not loaded on this device. Run 'openclaw matrix verify backup restore' to load it and restore old room keys.`
|
||||
|
||||
- Meaning: OpenClaw knows you should have a backup key, but it is not active on this device.
|
||||
- What to do: run `openclaw matrix verify backup restore`, or pass `--recovery-key` if needed.
|
||||
- What to do: run `openclaw matrix verify backup restore`, or set `MATRIX_RECOVERY_KEY` and run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin` if needed.
|
||||
|
||||
`Store a recovery key with 'openclaw matrix verify device <key>', then run 'openclaw matrix verify backup restore'.`
|
||||
`Store a recovery key with 'openclaw matrix verify device --recovery-key-stdin', then run 'openclaw matrix verify backup restore'.`
|
||||
|
||||
- Meaning: this device does not currently have the recovery key stored.
|
||||
- What to do: verify the device with your recovery key first, then restore the backup.
|
||||
- What to do: set `MATRIX_RECOVERY_KEY`, run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin`, then restore the backup.
|
||||
|
||||
`Backup key mismatch on this device. Re-run 'openclaw matrix verify device <key>' with the matching recovery key.`
|
||||
`Backup key mismatch on this device. Re-run 'openclaw matrix verify device --recovery-key-stdin' with the matching recovery key.`
|
||||
|
||||
- Meaning: the stored key does not match the active Matrix backup.
|
||||
- What to do: rerun `openclaw matrix verify device "<your-recovery-key>"` with the correct key.
|
||||
- What to do: set `MATRIX_RECOVERY_KEY` to the correct key and run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin`.
|
||||
|
||||
If you accept losing unrecoverable old encrypted history, you can instead reset the
|
||||
current backup baseline with `openclaw matrix verify backup reset --yes`. When the
|
||||
stored backup secret is broken, that reset may also recreate secret storage so the
|
||||
new backup key can load correctly after restart.
|
||||
|
||||
`Backup trust chain is not verified on this device. Re-run 'openclaw matrix verify device <key>'.`
|
||||
`Backup trust chain is not verified on this device. Re-run 'openclaw matrix verify device --recovery-key-stdin'.`
|
||||
|
||||
- Meaning: the backup exists, but this device does not trust the cross-signing chain strongly enough yet.
|
||||
- What to do: rerun `openclaw matrix verify device "<your-recovery-key>"`.
|
||||
- What to do: set `MATRIX_RECOVERY_KEY` and run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin`.
|
||||
|
||||
`Matrix recovery key is required`
|
||||
|
||||
- Meaning: you tried a recovery step without supplying a recovery key when one was required.
|
||||
- What to do: rerun the command with your recovery key.
|
||||
- What to do: rerun the command with `--recovery-key-stdin`, for example `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin`.
|
||||
|
||||
`Invalid Matrix recovery key: ...`
|
||||
|
||||
@@ -313,7 +317,7 @@ new backup key can load correctly after restart.
|
||||
- What to do: run `openclaw matrix verify self`, accept the request in another
|
||||
Matrix client, compare the SAS, and type `yes` only when it matches. The
|
||||
command waits for full Matrix identity trust before reporting success. Use
|
||||
`openclaw matrix verify bootstrap --recovery-key "<your-recovery-key>" --force-reset-cross-signing`
|
||||
`printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify bootstrap --recovery-key-stdin --force-reset-cross-signing`
|
||||
only when you intentionally want to replace the current cross-signing identity.
|
||||
|
||||
`Matrix key backup is not active on this device after loading from secret storage.`
|
||||
@@ -321,10 +325,10 @@ new backup key can load correctly after restart.
|
||||
- Meaning: secret storage did not produce an active backup session on this device.
|
||||
- What to do: verify the device first, then recheck with `openclaw matrix verify backup status`.
|
||||
|
||||
`Matrix crypto backend cannot load backup keys from secret storage. Verify this device with 'openclaw matrix verify device <key>' first.`
|
||||
`Matrix crypto backend cannot load backup keys from secret storage. Verify this device with 'openclaw matrix verify device --recovery-key-stdin' first.`
|
||||
|
||||
- Meaning: this device cannot restore from secret storage until device verification is complete.
|
||||
- What to do: run `openclaw matrix verify device "<your-recovery-key>"` first.
|
||||
- What to do: run `printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify device --recovery-key-stdin` first.
|
||||
|
||||
### Custom plugin install messages
|
||||
|
||||
@@ -340,7 +344,7 @@ Run these checks in order:
|
||||
```bash
|
||||
openclaw matrix verify status --verbose
|
||||
openclaw matrix verify backup status --verbose
|
||||
openclaw matrix verify backup restore --recovery-key "<your-recovery-key>" --verbose
|
||||
printf '%s\n' "$MATRIX_RECOVERY_KEY" | openclaw matrix verify backup restore --recovery-key-stdin --verbose
|
||||
```
|
||||
|
||||
If the backup restores successfully but some old rooms are still missing history, those missing keys were probably never backed up by the previous plugin.
|
||||
|
||||
@@ -74,6 +74,10 @@ ReadWritePaths=/var/lib/openclaw /home/openclaw/.openclaw /tmp
|
||||
|
||||
If `OPENCLAW_PLUGIN_STAGE_DIR` is not set, OpenClaw uses `$STATE_DIRECTORY` when
|
||||
systemd provides it, then falls back to `~/.openclaw/plugin-runtime-deps`.
|
||||
The repair step treats that stage as an OpenClaw-owned local package root and
|
||||
ignores user npm prefix/global settings, so global-install npm config does not
|
||||
redirect bundled plugin dependencies into `~/node_modules` or the global package
|
||||
tree.
|
||||
|
||||
### Bundled plugin runtime dependencies
|
||||
|
||||
|
||||
283
docs/logging.md
283
docs/logging.md
@@ -1,14 +1,12 @@
|
||||
---
|
||||
summary: "Logging overview: file logs, console output, CLI tailing, and the Control UI"
|
||||
summary: "File logs, console output, CLI tailing, and the Control UI Logs tab"
|
||||
read_when:
|
||||
- You need a beginner-friendly overview of logging
|
||||
- You want to configure log levels or formats
|
||||
- You need a beginner-friendly overview of OpenClaw logging
|
||||
- You want to configure log levels, formats, or redaction
|
||||
- You are troubleshooting and need to find logs quickly
|
||||
title: "Logging overview"
|
||||
title: "Logging"
|
||||
---
|
||||
|
||||
# Logging
|
||||
|
||||
OpenClaw has two main log surfaces:
|
||||
|
||||
- **File logs** (JSON lines) written by the Gateway.
|
||||
@@ -171,264 +169,35 @@ Tool summaries can redact sensitive tokens before they hit the console:
|
||||
|
||||
Redaction affects **console output only** and does not alter file logs.
|
||||
|
||||
## Diagnostics + OpenTelemetry
|
||||
## Diagnostics and OpenTelemetry
|
||||
|
||||
Diagnostics are structured, machine-readable events for model runs **and**
|
||||
Diagnostics are structured, machine-readable events for model runs and
|
||||
message-flow telemetry (webhooks, queueing, session state). They do **not**
|
||||
replace logs; they exist to feed metrics, traces, and other exporters.
|
||||
replace logs — they feed metrics, traces, and exporters. Events are emitted
|
||||
in-process whether or not you export them.
|
||||
|
||||
Diagnostics events are emitted in-process, but exporters only attach when
|
||||
diagnostics + the exporter plugin are enabled.
|
||||
Two adjacent surfaces:
|
||||
|
||||
### OpenTelemetry vs OTLP
|
||||
- **OpenTelemetry export** — send metrics, traces, and logs over OTLP/HTTP to
|
||||
any OpenTelemetry-compatible collector or backend (Grafana, Datadog,
|
||||
Honeycomb, New Relic, Tempo, etc.). Full configuration, signal catalog,
|
||||
metric/span names, env vars, and privacy model live on a dedicated page:
|
||||
[OpenTelemetry export](/gateway/opentelemetry).
|
||||
- **Diagnostics flags** — targeted debug-log flags that route extra logs to
|
||||
`logging.file` without raising `logging.level`. Flags are case-insensitive
|
||||
and support wildcards (`telegram.*`, `*`). Configure under `diagnostics.flags`
|
||||
or via the `OPENCLAW_DIAGNOSTICS=...` env override. Full guide:
|
||||
[Diagnostics flags](/diagnostics/flags).
|
||||
|
||||
- **OpenTelemetry (OTel)**: the data model + SDKs for traces, metrics, and logs.
|
||||
- **OTLP**: the wire protocol used to export OTel data to a collector/backend.
|
||||
- OpenClaw exports via **OTLP/HTTP (protobuf)** today.
|
||||
To enable diagnostics events for plugins or custom sinks without OTLP export:
|
||||
|
||||
### Signals exported
|
||||
|
||||
- **Metrics**: counters + histograms (token usage, message flow, queueing).
|
||||
- **Traces**: spans for model usage + webhook/message processing.
|
||||
- **Logs**: exported over OTLP when `diagnostics.otel.logs` is enabled. Log
|
||||
volume can be high; keep `logging.level` and exporter filters in mind.
|
||||
|
||||
### Diagnostic event catalog
|
||||
|
||||
Model usage:
|
||||
|
||||
- `model.usage`: tokens, cost, duration, context, provider/model/channel, session ids.
|
||||
|
||||
Message flow:
|
||||
|
||||
- `webhook.received`: webhook ingress per channel.
|
||||
- `webhook.processed`: webhook handled + duration.
|
||||
- `webhook.error`: webhook handler errors.
|
||||
- `message.queued`: message enqueued for processing.
|
||||
- `message.processed`: outcome + duration + optional error.
|
||||
- `message.delivery.started`: outbound delivery attempt started.
|
||||
- `message.delivery.completed`: outbound delivery attempt finished + duration/result count.
|
||||
- `message.delivery.error`: outbound delivery attempt failed + duration/bounded error category.
|
||||
|
||||
Queue + session:
|
||||
|
||||
- `queue.lane.enqueue`: command queue lane enqueue + depth.
|
||||
- `queue.lane.dequeue`: command queue lane dequeue + wait time.
|
||||
- `session.state`: session state transition + reason.
|
||||
- `session.stuck`: session stuck warning + age.
|
||||
- `run.attempt`: run retry/attempt metadata.
|
||||
- `diagnostic.heartbeat`: aggregate counters (webhooks/queue/session).
|
||||
|
||||
Exec:
|
||||
|
||||
- `exec.process.completed`: terminal exec process outcome, duration, target, mode,
|
||||
exit code, and failure kind. Command text and working directories are not
|
||||
included.
|
||||
|
||||
### Enable diagnostics (no exporter)
|
||||
|
||||
Use this if you want diagnostics events available to plugins or custom sinks:
|
||||
|
||||
```json
|
||||
```json5
|
||||
{
|
||||
"diagnostics": {
|
||||
"enabled": true
|
||||
}
|
||||
diagnostics: { enabled: true },
|
||||
}
|
||||
```
|
||||
|
||||
### Diagnostics flags (targeted logs)
|
||||
|
||||
Use flags to turn on extra, targeted debug logs without raising `logging.level`.
|
||||
Flags are case-insensitive and support wildcards (e.g. `telegram.*` or `*`).
|
||||
|
||||
```json
|
||||
{
|
||||
"diagnostics": {
|
||||
"flags": ["telegram.http"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Env override (one-off):
|
||||
|
||||
```
|
||||
OPENCLAW_DIAGNOSTICS=telegram.http,telegram.payload
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Flag logs go to the standard log file (same as `logging.file`).
|
||||
- Output is still redacted according to `logging.redactSensitive`.
|
||||
- Full guide: [/diagnostics/flags](/diagnostics/flags).
|
||||
|
||||
### Export to OpenTelemetry
|
||||
|
||||
Diagnostics can be exported via the `diagnostics-otel` plugin (OTLP/HTTP). This
|
||||
works with any OpenTelemetry collector/backend that accepts OTLP/HTTP.
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"allow": ["diagnostics-otel"],
|
||||
"entries": {
|
||||
"diagnostics-otel": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"diagnostics": {
|
||||
"enabled": true,
|
||||
"otel": {
|
||||
"enabled": true,
|
||||
"endpoint": "http://otel-collector:4318",
|
||||
"protocol": "http/protobuf",
|
||||
"serviceName": "openclaw-gateway",
|
||||
"traces": true,
|
||||
"metrics": true,
|
||||
"logs": true,
|
||||
"sampleRate": 0.2,
|
||||
"flushIntervalMs": 60000,
|
||||
"captureContent": {
|
||||
"enabled": false,
|
||||
"inputMessages": false,
|
||||
"outputMessages": false,
|
||||
"toolInputs": false,
|
||||
"toolOutputs": false,
|
||||
"systemPrompt": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- You can also enable the plugin with `openclaw plugins enable diagnostics-otel`.
|
||||
- `protocol` currently supports `http/protobuf` only. `grpc` is ignored.
|
||||
- Metrics include token usage, cost, context size, run duration, and message-flow
|
||||
counters/histograms (webhooks, queueing, session state, queue depth/wait).
|
||||
- Traces/metrics can be toggled with `traces` / `metrics` (default: on). Traces
|
||||
include model usage spans plus webhook/message processing spans when enabled.
|
||||
- Raw model/tool content is not exported by default. Use
|
||||
`diagnostics.otel.captureContent` only when your collector and retention policy
|
||||
are approved for prompt, response, tool, or system prompt text.
|
||||
- Set `headers` when your collector requires auth.
|
||||
- Environment variables supported: `OTEL_EXPORTER_OTLP_ENDPOINT`,
|
||||
`OTEL_SERVICE_NAME`, `OTEL_EXPORTER_OTLP_PROTOCOL`.
|
||||
- Set `OPENCLAW_OTEL_PRELOADED=1` when another preload or host process already
|
||||
registered the global OpenTelemetry SDK. In that mode the plugin does not start
|
||||
or shut down its own SDK, but it still wires OpenClaw diagnostic listeners and
|
||||
honors `diagnostics.otel.traces`, `metrics`, and `logs`.
|
||||
|
||||
### Exported metrics (names + types)
|
||||
|
||||
Model usage:
|
||||
|
||||
- `openclaw.tokens` (counter, attrs: `openclaw.token`, `openclaw.channel`,
|
||||
`openclaw.provider`, `openclaw.model`)
|
||||
- `openclaw.cost.usd` (counter, attrs: `openclaw.channel`, `openclaw.provider`,
|
||||
`openclaw.model`)
|
||||
- `openclaw.run.duration_ms` (histogram, attrs: `openclaw.channel`,
|
||||
`openclaw.provider`, `openclaw.model`)
|
||||
- `openclaw.context.tokens` (histogram, attrs: `openclaw.context`,
|
||||
`openclaw.channel`, `openclaw.provider`, `openclaw.model`)
|
||||
|
||||
Message flow:
|
||||
|
||||
- `openclaw.webhook.received` (counter, attrs: `openclaw.channel`,
|
||||
`openclaw.webhook`)
|
||||
- `openclaw.webhook.error` (counter, attrs: `openclaw.channel`,
|
||||
`openclaw.webhook`)
|
||||
- `openclaw.webhook.duration_ms` (histogram, attrs: `openclaw.channel`,
|
||||
`openclaw.webhook`)
|
||||
- `openclaw.message.queued` (counter, attrs: `openclaw.channel`,
|
||||
`openclaw.source`)
|
||||
- `openclaw.message.processed` (counter, attrs: `openclaw.channel`,
|
||||
`openclaw.outcome`)
|
||||
- `openclaw.message.duration_ms` (histogram, attrs: `openclaw.channel`,
|
||||
`openclaw.outcome`)
|
||||
- `openclaw.message.delivery.started` (counter, attrs: `openclaw.channel`,
|
||||
`openclaw.delivery.kind`)
|
||||
- `openclaw.message.delivery.duration_ms` (histogram, attrs:
|
||||
`openclaw.channel`, `openclaw.delivery.kind`, `openclaw.outcome`,
|
||||
`openclaw.errorCategory`)
|
||||
|
||||
Queues + sessions:
|
||||
|
||||
- `openclaw.queue.lane.enqueue` (counter, attrs: `openclaw.lane`)
|
||||
- `openclaw.queue.lane.dequeue` (counter, attrs: `openclaw.lane`)
|
||||
- `openclaw.queue.depth` (histogram, attrs: `openclaw.lane` or
|
||||
`openclaw.channel=heartbeat`)
|
||||
- `openclaw.queue.wait_ms` (histogram, attrs: `openclaw.lane`)
|
||||
- `openclaw.session.state` (counter, attrs: `openclaw.state`, `openclaw.reason`)
|
||||
- `openclaw.session.stuck` (counter, attrs: `openclaw.state`)
|
||||
- `openclaw.session.stuck_age_ms` (histogram, attrs: `openclaw.state`)
|
||||
- `openclaw.run.attempt` (counter, attrs: `openclaw.attempt`)
|
||||
|
||||
Exec:
|
||||
|
||||
- `openclaw.exec.duration_ms` (histogram, attrs: `openclaw.exec.target`,
|
||||
`openclaw.exec.mode`, `openclaw.outcome`, `openclaw.failureKind`)
|
||||
|
||||
### Exported spans (names + key attributes)
|
||||
|
||||
- `openclaw.model.usage`
|
||||
- `openclaw.channel`, `openclaw.provider`, `openclaw.model`
|
||||
- `openclaw.tokens.*` (input/output/cache_read/cache_write/total)
|
||||
- `openclaw.run`
|
||||
- `openclaw.outcome`, `openclaw.channel`, `openclaw.provider`,
|
||||
`openclaw.model`, `openclaw.errorCategory`
|
||||
- `openclaw.model.call`
|
||||
- `gen_ai.system`, `gen_ai.request.model`, `gen_ai.operation.name`,
|
||||
`openclaw.provider`, `openclaw.model`, `openclaw.api`,
|
||||
`openclaw.transport`
|
||||
- `openclaw.tool.execution`
|
||||
- `gen_ai.tool.name`, `openclaw.toolName`, `openclaw.errorCategory`,
|
||||
`openclaw.tool.params.*`
|
||||
- `openclaw.exec`
|
||||
- `openclaw.exec.target`, `openclaw.exec.mode`, `openclaw.outcome`,
|
||||
`openclaw.failureKind`, `openclaw.exec.command_length`,
|
||||
`openclaw.exec.exit_code`, `openclaw.exec.timed_out`
|
||||
- `openclaw.webhook.processed`
|
||||
- `openclaw.channel`, `openclaw.webhook`, `openclaw.chatId`
|
||||
- `openclaw.webhook.error`
|
||||
- `openclaw.channel`, `openclaw.webhook`, `openclaw.chatId`,
|
||||
`openclaw.error`
|
||||
- `openclaw.message.processed`
|
||||
- `openclaw.channel`, `openclaw.outcome`, `openclaw.chatId`,
|
||||
`openclaw.messageId`, `openclaw.reason`
|
||||
- `openclaw.message.delivery`
|
||||
- `openclaw.channel`, `openclaw.delivery.kind`, `openclaw.outcome`,
|
||||
`openclaw.errorCategory`, `openclaw.delivery.result_count`
|
||||
- `openclaw.session.stuck`
|
||||
- `openclaw.state`, `openclaw.ageMs`, `openclaw.queueDepth`
|
||||
|
||||
When content capture is explicitly enabled, model/tool spans can also include
|
||||
bounded, redacted `openclaw.content.*` attributes for the specific content
|
||||
classes you opted into.
|
||||
|
||||
### Sampling + flushing
|
||||
|
||||
- Trace sampling: `diagnostics.otel.sampleRate` (0.0–1.0, root spans only).
|
||||
- Metric export interval: `diagnostics.otel.flushIntervalMs` (min 1000ms).
|
||||
|
||||
### Protocol notes
|
||||
|
||||
- OTLP/HTTP endpoints can be set via `diagnostics.otel.endpoint` or
|
||||
`OTEL_EXPORTER_OTLP_ENDPOINT`.
|
||||
- If the endpoint already contains `/v1/traces` or `/v1/metrics`, it is used as-is.
|
||||
- If the endpoint already contains `/v1/logs`, it is used as-is for logs.
|
||||
- `OPENCLAW_OTEL_PRELOADED=1` reuses an externally registered OpenTelemetry SDK
|
||||
for traces/metrics instead of starting a plugin-owned NodeSDK.
|
||||
- `diagnostics.otel.logs` enables OTLP log export for the main logger output.
|
||||
|
||||
### Log export behavior
|
||||
|
||||
- OTLP logs use the same structured records written to `logging.file`.
|
||||
- Respect `logging.level` (file log level). Console redaction does **not** apply
|
||||
to OTLP logs.
|
||||
- High-volume installs should prefer OTLP collector sampling/filtering.
|
||||
For OTLP export to a collector, see [OpenTelemetry export](/gateway/opentelemetry).
|
||||
|
||||
## Troubleshooting tips
|
||||
|
||||
@@ -439,5 +208,7 @@ classes you opted into.
|
||||
|
||||
## Related
|
||||
|
||||
- [Gateway Logging Internals](/gateway/logging) — WS log styles, subsystem prefixes, and console capture
|
||||
- [Diagnostics](/gateway/configuration-reference#diagnostics) — OpenTelemetry export and cache trace config
|
||||
- [OpenTelemetry export](/gateway/opentelemetry) — OTLP/HTTP export, metric/span catalog, privacy model
|
||||
- [Diagnostics flags](/diagnostics/flags) — targeted debug-log flags
|
||||
- [Gateway logging internals](/gateway/logging) — WS log styles, subsystem prefixes, and console capture
|
||||
- [Configuration reference](/gateway/configuration-reference#diagnostics) — full `diagnostics.*` field reference
|
||||
|
||||
@@ -63,6 +63,7 @@ Supported keys:
|
||||
},
|
||||
system: {},
|
||||
},
|
||||
speechLocale: "ru-RU",
|
||||
silenceTimeoutMs: 1500,
|
||||
interruptOnSpeech: true,
|
||||
},
|
||||
@@ -78,6 +79,7 @@ Defaults:
|
||||
- `providers.elevenlabs.modelId`: defaults to `eleven_v3` when unset.
|
||||
- `providers.mlx.modelId`: defaults to `mlx-community/Soprano-80M-bf16` when unset.
|
||||
- `providers.elevenlabs.apiKey`: falls back to `ELEVENLABS_API_KEY` (or gateway shell profile if available).
|
||||
- `speechLocale`: optional BCP 47 locale id for on-device Talk speech recognition on iOS/macOS. Leave unset to use the device default.
|
||||
- `outputFormat`: defaults to `pcm_44100` on macOS/iOS and `pcm_24000` on Android (set `mp3_*` to force MP3 streaming)
|
||||
|
||||
## macOS UI
|
||||
@@ -91,6 +93,13 @@ Defaults:
|
||||
- Click cloud: stop speaking
|
||||
- Click X: exit Talk mode
|
||||
|
||||
## Android UI
|
||||
|
||||
- Voice tab toggle: **Talk**
|
||||
- Manual **Mic** and **Talk** are mutually exclusive runtime capture modes.
|
||||
- Manual Mic stops when the app leaves the foreground or the user leaves the Voice tab.
|
||||
- Talk Mode keeps running until toggled off or the Android node disconnects, and uses Android's microphone foreground-service type while active.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires Speech + Microphone permissions.
|
||||
|
||||
@@ -199,8 +199,10 @@ See [Camera node](/nodes/camera) for parameters and CLI helpers.
|
||||
|
||||
### 8) Voice + expanded Android command surface
|
||||
|
||||
- Voice: Android uses a single mic on/off flow in the Voice tab with transcript capture and `talk.speak` playback. Local system TTS is used only when `talk.speak` is unavailable. Voice stops when the app leaves the foreground.
|
||||
- Voice wake/talk-mode toggles are currently removed from Android UX/runtime.
|
||||
- Voice tab: Android has two explicit capture modes. **Mic** is a manual Voice-tab session that sends each pause as a chat turn and stops when the app leaves the foreground or the user leaves the Voice tab. **Talk** is continuous Talk Mode and keeps listening until toggled off or the node disconnects.
|
||||
- Talk Mode promotes the existing foreground service from `dataSync` to `dataSync|microphone` before capture starts, then demotes it when Talk Mode stops. Android 14+ requires the `FOREGROUND_SERVICE_MICROPHONE` declaration, the `RECORD_AUDIO` runtime grant, and the microphone service type at runtime.
|
||||
- Spoken replies use `talk.speak` through the configured gateway Talk provider. Local system TTS is used only when `talk.speak` is unavailable.
|
||||
- Voice wake remains disabled in the Android UX/runtime.
|
||||
- Additional Android command families (availability depends on device + permissions):
|
||||
- `device.status`, `device.info`, `device.permissions`, `device.health`
|
||||
- `notifications.list`, `notifications.actions` (see [Notification forwarding](#notification-forwarding) below)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user