mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-14 01:58:47 +08:00
Compare commits
2 Commits
fix/codeql
...
codex/maco
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02ff01c3c5 | ||
|
|
bc94742ccb |
@@ -255,27 +255,12 @@ loop. The router:
|
||||
- never merges autofix PRs or draft PRs;
|
||||
- merges automerge PRs only when ClawSweeper passed the exact current head,
|
||||
checks are green, GitHub says mergeable, no human-review label is present,
|
||||
the PR is not draft, required user-facing OpenClaw changelog entries are
|
||||
present, and both merge gates are open.
|
||||
the PR is not draft, and both merge gates are open.
|
||||
|
||||
If ClawSweeper passes while merge gates are closed, it labels
|
||||
`clawsweeper:merge-ready` and comments instead of merging. `@clawsweeper stop`
|
||||
adds `clawsweeper:human-review`.
|
||||
|
||||
When Peter asks Codex to create a PR and enable ClawSweeper automerge, do not
|
||||
leave his local OpenClaw checkout on the PR branch. After the PR is created,
|
||||
pushed, and the `@clawsweeper automerge` request is posted or otherwise
|
||||
confirmed, return the local checkout to `main` and fast-forward it when the
|
||||
working tree is clean:
|
||||
|
||||
```bash
|
||||
git switch main
|
||||
git pull --ff-only
|
||||
```
|
||||
|
||||
If unrelated local edits or an in-progress rebase prevent switching, report the
|
||||
blocker instead of stashing, deleting, or overwriting work.
|
||||
|
||||
Repair caps:
|
||||
|
||||
```bash
|
||||
@@ -285,17 +270,13 @@ CLAWSWEEPER_MAX_REPAIRS_PER_HEAD=1
|
||||
|
||||
## Security Boundary
|
||||
|
||||
Do not stage unapproved security-sensitive work for ClawSweeper Repair. Route
|
||||
vulnerability reports, CVE/GHSA/advisory work, leaked secrets/tokens/keys,
|
||||
plaintext secret storage, SSRF, XSS, CSRF, RCE, auth bypass, privilege
|
||||
escalation, and sensitive data exposure to central OpenClaw security handling.
|
||||
Do not stage security-sensitive work for ClawSweeper Repair. Route vulnerability
|
||||
reports, CVE/GHSA/advisory work, leaked secrets/tokens/keys, plaintext secret
|
||||
storage, SSRF, XSS, CSRF, RCE, auth bypass, privilege escalation, and sensitive
|
||||
data exposure to central OpenClaw security handling.
|
||||
|
||||
For PRs explicitly opted into `clawsweeper:autofix` or
|
||||
`clawsweeper:automerge`, security-sensitive review findings may dispatch
|
||||
bounded repair, but merge remains blocked until a later exact-head review is
|
||||
clean and the normal merge gates pass. Trust deterministic ClawSweeper security
|
||||
markers, labels, and job frontmatter; do not infer security handling from vague
|
||||
prose.
|
||||
For adopted automerge jobs, trust deterministic ClawSweeper security markers,
|
||||
labels, and job frontmatter; do not infer security handling from vague prose.
|
||||
|
||||
## Monitoring
|
||||
|
||||
|
||||
@@ -41,28 +41,6 @@ gitcrawl cluster-detail openclaw/openclaw --id <cluster-id> --member-limit 20 --
|
||||
- `invalid`
|
||||
- `dirty` for PRs only
|
||||
|
||||
## Select small high-confidence triage candidates
|
||||
|
||||
When asked for `X` issues or PRs to triage, `X` means qualified candidates, not sampled threads.
|
||||
|
||||
Only list candidates that pass all gates:
|
||||
|
||||
- small owner/surface, with a likely narrow fix and focused regression test
|
||||
- symptom is reproducible or provable with logs, failing test, live command, dependency contract, or current-main behavior
|
||||
- root cause is traceable to code with file/line and the proposed fix touches that path
|
||||
- no strong smell that a broader refactor, ownership rethink, migration, or product decision is the better fix
|
||||
- dependency-backed behavior checked against upstream docs/source/types; live or web proof used when local proof is insufficient
|
||||
|
||||
Loop:
|
||||
|
||||
1. Use `gitcrawl` / `gh` to gather candidate clusters.
|
||||
2. Read issue/PR body, comments, current code, adjacent tests, and dependency contracts.
|
||||
3. Try focused repro or proof.
|
||||
4. Reject unclear, stale, speculative, broad-refactor, or owner-ambiguous items.
|
||||
5. Continue until `X` qualified candidates or the bounded search is exhausted.
|
||||
|
||||
Output only qualifying candidates, with: ref, surface, proof, cause, fix sketch, why small, expected test/gate. If none qualify, say so; do not pad.
|
||||
|
||||
## Enforce the bug-fix evidence bar
|
||||
|
||||
- Never merge a bug-fix PR based only on issue text, PR text, or AI rationale.
|
||||
|
||||
86
.github/CODEOWNERS
vendored
86
.github/CODEOWNERS
vendored
@@ -2,51 +2,51 @@
|
||||
/.github/CODEOWNERS @steipete
|
||||
|
||||
# WARNING: GitHub CODEOWNERS uses last-match-wins semantics.
|
||||
# If you add overlapping rules below the secops block, include @openclaw/openclaw-secops
|
||||
# If you add overlapping rules below the secops block, include @openclaw/secops
|
||||
# on those entries too or you can silently remove required secops review.
|
||||
# Security-sensitive code, config, and docs require secops review.
|
||||
/SECURITY.md @openclaw/openclaw-secops
|
||||
/.github/dependabot.yml @openclaw/openclaw-secops
|
||||
/.github/codeql/ @openclaw/openclaw-secops
|
||||
/.github/workflows/codeql.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/codeql-android-critical-security.yml @openclaw/openclaw-secops
|
||||
/.github/workflows/codeql-critical-quality.yml @openclaw/openclaw-secops
|
||||
/src/security/ @openclaw/openclaw-secops
|
||||
/src/secrets/ @openclaw/openclaw-secops
|
||||
/src/config/*secret*.ts @openclaw/openclaw-secops
|
||||
/src/config/**/*secret*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/*auth*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/**/*auth*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/*secret*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/**/*secret*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/security-path*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/resolve-configured-secret-input-string*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/protocol/**/*secret*.ts @openclaw/openclaw-secops
|
||||
/src/gateway/server-methods/secrets*.ts @openclaw/openclaw-secops
|
||||
/src/agents/*auth*.ts @openclaw/openclaw-secops
|
||||
/src/agents/**/*auth*.ts @openclaw/openclaw-secops
|
||||
/src/agents/auth-profiles*.ts @openclaw/openclaw-secops
|
||||
/src/agents/auth-health*.ts @openclaw/openclaw-secops
|
||||
/src/agents/auth-profiles/ @openclaw/openclaw-secops
|
||||
/src/agents/sandbox.ts @openclaw/openclaw-secops
|
||||
/src/agents/sandbox-*.ts @openclaw/openclaw-secops
|
||||
/src/agents/sandbox/ @openclaw/openclaw-secops
|
||||
/src/infra/secret-file*.ts @openclaw/openclaw-secops
|
||||
/src/cron/stagger.ts @openclaw/openclaw-secops
|
||||
/src/cron/service/jobs.ts @openclaw/openclaw-secops
|
||||
/docs/security/ @openclaw/openclaw-secops
|
||||
/docs/gateway/authentication.md @openclaw/openclaw-secops
|
||||
/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @openclaw/openclaw-secops
|
||||
/docs/gateway/sandboxing.md @openclaw/openclaw-secops
|
||||
/docs/gateway/secrets-plan-contract.md @openclaw/openclaw-secops
|
||||
/docs/gateway/secrets.md @openclaw/openclaw-secops
|
||||
/docs/gateway/security/ @openclaw/openclaw-secops
|
||||
/docs/cli/approvals.md @openclaw/openclaw-secops
|
||||
/docs/cli/sandbox.md @openclaw/openclaw-secops
|
||||
/docs/cli/security.md @openclaw/openclaw-secops
|
||||
/docs/cli/secrets.md @openclaw/openclaw-secops
|
||||
/docs/reference/secretref-credential-surface.md @openclaw/openclaw-secops
|
||||
/docs/reference/secretref-user-supplied-credentials-matrix.json @openclaw/openclaw-secops
|
||||
/SECURITY.md @openclaw/secops
|
||||
/.github/dependabot.yml @openclaw/secops
|
||||
/.github/codeql/ @openclaw/secops
|
||||
/.github/workflows/codeql.yml @openclaw/secops
|
||||
/.github/workflows/codeql-android-critical-security.yml @openclaw/secops
|
||||
/.github/workflows/codeql-critical-quality.yml @openclaw/secops
|
||||
/src/security/ @openclaw/secops
|
||||
/src/secrets/ @openclaw/secops
|
||||
/src/config/*secret*.ts @openclaw/secops
|
||||
/src/config/**/*secret*.ts @openclaw/secops
|
||||
/src/gateway/*auth*.ts @openclaw/secops
|
||||
/src/gateway/**/*auth*.ts @openclaw/secops
|
||||
/src/gateway/*secret*.ts @openclaw/secops
|
||||
/src/gateway/**/*secret*.ts @openclaw/secops
|
||||
/src/gateway/security-path*.ts @openclaw/secops
|
||||
/src/gateway/resolve-configured-secret-input-string*.ts @openclaw/secops
|
||||
/src/gateway/protocol/**/*secret*.ts @openclaw/secops
|
||||
/src/gateway/server-methods/secrets*.ts @openclaw/secops
|
||||
/src/agents/*auth*.ts @openclaw/secops
|
||||
/src/agents/**/*auth*.ts @openclaw/secops
|
||||
/src/agents/auth-profiles*.ts @openclaw/secops
|
||||
/src/agents/auth-health*.ts @openclaw/secops
|
||||
/src/agents/auth-profiles/ @openclaw/secops
|
||||
/src/agents/sandbox.ts @openclaw/secops
|
||||
/src/agents/sandbox-*.ts @openclaw/secops
|
||||
/src/agents/sandbox/ @openclaw/secops
|
||||
/src/infra/secret-file*.ts @openclaw/secops
|
||||
/src/cron/stagger.ts @openclaw/secops
|
||||
/src/cron/service/jobs.ts @openclaw/secops
|
||||
/docs/security/ @openclaw/secops
|
||||
/docs/gateway/authentication.md @openclaw/secops
|
||||
/docs/gateway/sandbox-vs-tool-policy-vs-elevated.md @openclaw/secops
|
||||
/docs/gateway/sandboxing.md @openclaw/secops
|
||||
/docs/gateway/secrets-plan-contract.md @openclaw/secops
|
||||
/docs/gateway/secrets.md @openclaw/secops
|
||||
/docs/gateway/security/ @openclaw/secops
|
||||
/docs/cli/approvals.md @openclaw/secops
|
||||
/docs/cli/sandbox.md @openclaw/secops
|
||||
/docs/cli/security.md @openclaw/secops
|
||||
/docs/cli/secrets.md @openclaw/secops
|
||||
/docs/reference/secretref-credential-surface.md @openclaw/secops
|
||||
/docs/reference/secretref-user-supplied-credentials-matrix.json @openclaw/secops
|
||||
|
||||
# Release workflow and its supporting release-path checks.
|
||||
/.github/workflows/openclaw-npm-release.yml @openclaw/openclaw-release-managers
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
name: openclaw-codeql-actions-critical-security
|
||||
|
||||
disable-default-queries: true
|
||||
|
||||
queries:
|
||||
- uses: security-extended
|
||||
|
||||
query-filters:
|
||||
- include:
|
||||
precision:
|
||||
- high
|
||||
- very-high
|
||||
tags contain: security
|
||||
security-severity: /([7-9]|10)\.(\d)+/
|
||||
|
||||
paths:
|
||||
- .github/actions
|
||||
- .github/workflows
|
||||
|
||||
@@ -14,29 +14,6 @@ query-filters:
|
||||
- security
|
||||
|
||||
paths:
|
||||
- extensions/bluebubbles/src
|
||||
- extensions/discord/src
|
||||
- extensions/feishu/src
|
||||
- extensions/googlechat/src
|
||||
- extensions/imessage/src
|
||||
- extensions/irc/src
|
||||
- extensions/line/src
|
||||
- extensions/matrix/src
|
||||
- extensions/mattermost/src
|
||||
- extensions/msteams/src
|
||||
- extensions/nextcloud-talk/src
|
||||
- extensions/nostr/src
|
||||
- extensions/qa-channel/src
|
||||
- extensions/qqbot/src
|
||||
- extensions/signal/src
|
||||
- extensions/slack/src
|
||||
- extensions/synology-chat/src
|
||||
- extensions/telegram/src
|
||||
- extensions/tlon/src
|
||||
- extensions/twitch/src
|
||||
- extensions/whatsapp/src
|
||||
- extensions/zalo/src
|
||||
- extensions/zalouser/src
|
||||
- src/channels
|
||||
|
||||
paths-ignore:
|
||||
|
||||
@@ -10,8 +10,10 @@ query-filters:
|
||||
precision:
|
||||
- high
|
||||
- very-high
|
||||
tags contain: security
|
||||
security-severity: /([7-9]|10)\.(\d)+/
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- recommendation
|
||||
- warning
|
||||
|
||||
paths:
|
||||
- src/channels
|
||||
|
||||
@@ -10,8 +10,10 @@ query-filters:
|
||||
precision:
|
||||
- high
|
||||
- very-high
|
||||
tags contain: security
|
||||
security-severity: /([7-9]|10)\.(\d)+/
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- recommendation
|
||||
- warning
|
||||
|
||||
paths:
|
||||
- src/agents/*auth*.ts
|
||||
|
||||
@@ -14,11 +14,8 @@ query-filters:
|
||||
- security
|
||||
|
||||
paths:
|
||||
- src/gateway/method-scopes.ts
|
||||
- src/gateway/protocol
|
||||
- src/gateway/server-methods
|
||||
- src/gateway/server-methods.ts
|
||||
- src/gateway/server-methods-list.ts
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
|
||||
@@ -10,8 +10,10 @@ query-filters:
|
||||
precision:
|
||||
- high
|
||||
- very-high
|
||||
tags contain: security
|
||||
security-severity: /([7-9]|10)\.(\d)+/
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- recommendation
|
||||
- warning
|
||||
|
||||
paths:
|
||||
- src/mcp
|
||||
|
||||
@@ -10,8 +10,10 @@ query-filters:
|
||||
precision:
|
||||
- high
|
||||
- very-high
|
||||
tags contain: security
|
||||
security-severity: /([7-9]|10)\.(\d)+/
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- recommendation
|
||||
- warning
|
||||
|
||||
paths:
|
||||
- src/infra/net
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
name: openclaw-codeql-plugin-sdk-reply-runtime-critical-quality
|
||||
|
||||
disable-default-queries: true
|
||||
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
|
||||
query-filters:
|
||||
- include:
|
||||
problem.severity:
|
||||
- error
|
||||
- exclude:
|
||||
tags:
|
||||
- security
|
||||
|
||||
paths:
|
||||
- src/plugin-sdk/inbound-envelope.ts
|
||||
- src/plugin-sdk/inbound-reply-dispatch.ts
|
||||
- src/plugin-sdk/reply-*.ts
|
||||
- src/plugin-sdk/channel-reply-*.ts
|
||||
- src/plugin-sdk/delivery-queue-runtime.ts
|
||||
- src/plugin-sdk/outbound-runtime.ts
|
||||
- src/plugin-sdk/outbound-send-deps.ts
|
||||
- src/plugin-sdk/model-session-runtime.ts
|
||||
- src/plugin-sdk/session-*.ts
|
||||
- src/plugin-sdk/thread-bindings-runtime.ts
|
||||
- src/plugin-sdk/thread-bindings-session-runtime.ts
|
||||
- src/plugin-sdk/conversation-binding-runtime.ts
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
- "**/coverage"
|
||||
- "**/*.generated.ts"
|
||||
- "**/*.bundle.js"
|
||||
- "**/*-runtime.js"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/*.e2e.test.ts"
|
||||
- "**/*.e2e.test.tsx"
|
||||
- "**/*test-support*"
|
||||
- "**/*test-helper*"
|
||||
- "**/*mock*"
|
||||
- "**/*fixture*"
|
||||
- "**/*bench*"
|
||||
@@ -10,8 +10,10 @@ query-filters:
|
||||
precision:
|
||||
- high
|
||||
- very-high
|
||||
tags contain: security
|
||||
security-severity: /([7-9]|10)\.(\d)+/
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- recommendation
|
||||
- warning
|
||||
|
||||
paths:
|
||||
- src/cli/plugin-install-config-policy.ts
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
name: openclaw-codeql-provider-runtime-boundary-critical-quality
|
||||
|
||||
disable-default-queries: true
|
||||
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
|
||||
query-filters:
|
||||
- include:
|
||||
problem.severity:
|
||||
- error
|
||||
- exclude:
|
||||
tags:
|
||||
- security
|
||||
|
||||
paths:
|
||||
- src/model-catalog
|
||||
- src/plugins/provider-*.ts
|
||||
- src/plugins/providers*.ts
|
||||
- src/plugins/*provider*.ts
|
||||
- src/plugins/capability-provider-runtime.ts
|
||||
- src/plugins/compaction-provider.ts
|
||||
- src/plugins/memory-embedding-provider*.ts
|
||||
- src/plugins/memory-embedding-providers*.ts
|
||||
- src/plugins/migration-provider-runtime.ts
|
||||
- src/plugins/synthetic-auth.runtime.ts
|
||||
- src/plugins/web-fetch-providers*.ts
|
||||
- src/plugins/web-search-providers*.ts
|
||||
|
||||
paths-ignore:
|
||||
- "**/node_modules"
|
||||
- "**/coverage"
|
||||
- "**/*.generated.ts"
|
||||
- "**/*.bundle.js"
|
||||
- "**/*-runtime.js"
|
||||
- "**/*.test.ts"
|
||||
- "**/*.test.tsx"
|
||||
- "**/*.e2e.test.ts"
|
||||
- "**/*.e2e.test.tsx"
|
||||
- "**/*test-support*"
|
||||
- "**/*test-helper*"
|
||||
- "**/*mock*"
|
||||
- "**/*fixture*"
|
||||
- "**/*bench*"
|
||||
6
.github/labeler.yml
vendored
6
.github/labeler.yml
vendored
@@ -9,12 +9,6 @@
|
||||
- "extensions/azure-speech/**"
|
||||
- "docs/providers/azure-speech.md"
|
||||
- "docs/tools/tts.md"
|
||||
"plugin: file-transfer":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/file-transfer/**"
|
||||
- "docs/nodes/index.md"
|
||||
- "docs/plugins/sdk-runtime.md"
|
||||
"channel: discord":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
|
||||
354
.github/workflows/codeql-critical-quality.yml
vendored
354
.github/workflows/codeql-critical-quality.yml
vendored
@@ -10,127 +10,14 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- agent-runtime-boundary
|
||||
- config-boundary
|
||||
- core-auth-secrets
|
||||
- channel-runtime-boundary
|
||||
- gateway-runtime-boundary
|
||||
- memory-runtime-boundary
|
||||
- mcp-process-runtime-boundary
|
||||
- plugin-boundary
|
||||
- plugin-sdk-package-contract
|
||||
- plugin-sdk-reply-runtime
|
||||
- provider-runtime-boundary
|
||||
- session-diagnostics-boundary
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- ".github/codeql/**"
|
||||
- ".github/workflows/codeql-critical-quality.yml"
|
||||
- "packages/plugin-package-contract/**"
|
||||
- "packages/plugin-sdk/**"
|
||||
- "packages/memory-host-sdk/**"
|
||||
- "src/config/**"
|
||||
- "extensions/bluebubbles/src/**"
|
||||
- "extensions/discord/src/**"
|
||||
- "extensions/feishu/src/**"
|
||||
- "extensions/googlechat/src/**"
|
||||
- "extensions/imessage/src/**"
|
||||
- "extensions/irc/src/**"
|
||||
- "extensions/line/src/**"
|
||||
- "extensions/matrix/src/**"
|
||||
- "extensions/mattermost/src/**"
|
||||
- "extensions/msteams/src/**"
|
||||
- "extensions/nextcloud-talk/src/**"
|
||||
- "extensions/nostr/src/**"
|
||||
- "extensions/qa-channel/src/**"
|
||||
- "extensions/qqbot/src/**"
|
||||
- "extensions/signal/src/**"
|
||||
- "extensions/slack/src/**"
|
||||
- "extensions/synology-chat/src/**"
|
||||
- "extensions/telegram/src/**"
|
||||
- "extensions/tlon/src/**"
|
||||
- "extensions/twitch/src/**"
|
||||
- "extensions/whatsapp/src/**"
|
||||
- "extensions/zalo/src/**"
|
||||
- "extensions/zalouser/src/**"
|
||||
- "src/agents/*auth*.ts"
|
||||
- "src/agents/**/*auth*.ts"
|
||||
- "src/agents/auth-health*.ts"
|
||||
- "src/agents/auth-profiles"
|
||||
- "src/agents/auth-profiles/**"
|
||||
- "src/agents/bash-tools.exec-host-shared.ts"
|
||||
- "src/agents/sandbox"
|
||||
- "src/agents/sandbox/**"
|
||||
- "src/agents/sandbox.ts"
|
||||
- "src/agents/sandbox-*.ts"
|
||||
- "src/acp/control-plane/**"
|
||||
- "src/agents/cli-runner/**"
|
||||
- "src/agents/command/**"
|
||||
- "src/agents/pi-embedded-runner/**"
|
||||
- "src/agents/tools/**"
|
||||
- "src/agents/*completion*.ts"
|
||||
- "src/agents/*transport*.ts"
|
||||
- "src/agents/model-*.ts"
|
||||
- "src/agents/openclaw-tools*.ts"
|
||||
- "src/agents/provider-*.ts"
|
||||
- "src/agents/session*.ts"
|
||||
- "src/agents/tool-call*.ts"
|
||||
- "src/auto-reply/reply/agent-runner*.ts"
|
||||
- "src/auto-reply/reply/commands*.ts"
|
||||
- "src/auto-reply/reply/directive-handling*.ts"
|
||||
- "src/auto-reply/reply/dispatch-*.ts"
|
||||
- "src/auto-reply/reply/get-reply-run*.ts"
|
||||
- "src/auto-reply/reply/provider-dispatcher*.ts"
|
||||
- "src/auto-reply/reply/queue*.ts"
|
||||
- "src/auto-reply/reply/reply-run-registry*.ts"
|
||||
- "src/auto-reply/reply/session*.ts"
|
||||
- "src/channels/**"
|
||||
- "src/auto-reply/reply/post-compaction-context.ts"
|
||||
- "src/auto-reply/reply/queue/**"
|
||||
- "src/auto-reply/reply/startup-context.ts"
|
||||
- "src/commands/doctor-cron-dreaming-payload-migration.ts"
|
||||
- "src/commands/doctor-memory-search.ts"
|
||||
- "src/commands/doctor-session-*.ts"
|
||||
- "src/commands/session-store-targets.ts"
|
||||
- "src/commands/sessions*.ts"
|
||||
- "src/cron/service/jobs.ts"
|
||||
- "src/cron/stagger.ts"
|
||||
- "src/gateway/*auth*.ts"
|
||||
- "src/gateway/**/*auth*.ts"
|
||||
- "src/gateway/*secret*.ts"
|
||||
- "src/gateway/**/*secret*.ts"
|
||||
- "src/gateway/protocol/**/*secret*.ts"
|
||||
- "src/gateway/resolve-configured-secret-input-string*.ts"
|
||||
- "src/gateway/security-path*.ts"
|
||||
- "src/gateway/server-methods/secrets*.ts"
|
||||
- "src/gateway/server-startup-memory.ts"
|
||||
- "src/gateway/method-scopes.ts"
|
||||
- "src/gateway/protocol/**"
|
||||
- "src/gateway/server-methods/**"
|
||||
- "src/gateway/server-methods.ts"
|
||||
- "src/gateway/server-methods-list.ts"
|
||||
- "src/infra/diagnostic-*.ts"
|
||||
- "src/infra/diagnostics-timeline.ts"
|
||||
- "src/infra/outbound/**"
|
||||
- "src/infra/secret-file*.ts"
|
||||
- "src/infra/session-delivery-queue*.ts"
|
||||
- "src/logging/diagnostic*.ts"
|
||||
- "src/memory/**"
|
||||
- "src/memory-host-sdk/**"
|
||||
- "src/mcp/**"
|
||||
- "src/model-catalog/**"
|
||||
- "src/plugin-sdk/**"
|
||||
- "src/plugins/**"
|
||||
- "src/process/**"
|
||||
- "src/secrets/**"
|
||||
- "src/security/**"
|
||||
schedule:
|
||||
- cron: "30 6 * * *"
|
||||
|
||||
concurrency:
|
||||
group: codeql-critical-quality-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event_name == 'pull_request' && github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
group: codeql-critical-quality-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
@@ -138,170 +25,12 @@ env:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
pull-requests: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
quality-shards:
|
||||
name: Select Critical Quality shards
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
agent: ${{ steps.detect.outputs.agent }}
|
||||
channel: ${{ steps.detect.outputs.channel }}
|
||||
config: ${{ steps.detect.outputs.config }}
|
||||
core_auth_secrets: ${{ steps.detect.outputs.core_auth_secrets }}
|
||||
gateway: ${{ steps.detect.outputs.gateway }}
|
||||
memory: ${{ steps.detect.outputs.memory }}
|
||||
mcp_process: ${{ steps.detect.outputs.mcp_process }}
|
||||
plugin: ${{ steps.detect.outputs.plugin }}
|
||||
plugin_sdk_package: ${{ steps.detect.outputs.plugin_sdk_package }}
|
||||
plugin_sdk_reply: ${{ steps.detect.outputs.plugin_sdk_reply }}
|
||||
provider: ${{ steps.detect.outputs.provider }}
|
||||
session_diagnostics: ${{ steps.detect.outputs.session_diagnostics }}
|
||||
steps:
|
||||
- name: Detect PR shard paths
|
||||
id: detect
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
agent=false
|
||||
channel=false
|
||||
config=false
|
||||
core_auth_secrets=false
|
||||
gateway=false
|
||||
memory=false
|
||||
mcp_process=false
|
||||
plugin=false
|
||||
plugin_sdk_package=false
|
||||
plugin_sdk_reply=false
|
||||
provider=false
|
||||
session_diagnostics=false
|
||||
|
||||
if [[ "${EVENT_NAME}" != "pull_request" ]]; then
|
||||
agent=true
|
||||
channel=true
|
||||
config=true
|
||||
core_auth_secrets=true
|
||||
gateway=true
|
||||
memory=true
|
||||
mcp_process=true
|
||||
plugin=true
|
||||
plugin_sdk_package=true
|
||||
plugin_sdk_reply=true
|
||||
provider=true
|
||||
session_diagnostics=true
|
||||
else
|
||||
while IFS= read -r file; do
|
||||
case "${file}" in
|
||||
.github/codeql/*|.github/workflows/codeql-critical-quality.yml)
|
||||
agent=true
|
||||
channel=true
|
||||
config=true
|
||||
core_auth_secrets=true
|
||||
gateway=true
|
||||
memory=true
|
||||
mcp_process=true
|
||||
plugin=true
|
||||
plugin_sdk_package=true
|
||||
plugin_sdk_reply=true
|
||||
provider=true
|
||||
session_diagnostics=true
|
||||
;;
|
||||
src/acp/control-plane/*|src/agents/cli-runner/*|src/agents/command/*|src/agents/pi-embedded-runner/*|src/agents/tools/*|src/agents/*completion*.ts|src/agents/*transport*.ts|src/agents/model-*.ts|src/agents/openclaw-tools*.ts|src/agents/provider-*.ts|src/agents/session*.ts|src/agents/tool-call*.ts|src/auto-reply/reply/agent-runner*.ts|src/auto-reply/reply/commands*.ts|src/auto-reply/reply/directive-handling*.ts|src/auto-reply/reply/dispatch-*.ts|src/auto-reply/reply/get-reply-run*.ts|src/auto-reply/reply/provider-dispatcher*.ts|src/auto-reply/reply/queue*.ts|src/auto-reply/reply/reply-run-registry*.ts|src/auto-reply/reply/session*.ts)
|
||||
agent=true
|
||||
;;
|
||||
src/auto-reply/reply/post-compaction-context.ts|src/auto-reply/reply/queue/*|src/auto-reply/reply/startup-context.ts|src/commands/doctor-session-*.ts|src/commands/session-store-targets.ts|src/commands/sessions*.ts|src/infra/diagnostic-*.ts|src/infra/diagnostics-timeline.ts|src/infra/session-delivery-queue*.ts|src/logging/diagnostic*.ts)
|
||||
session_diagnostics=true
|
||||
;;
|
||||
extensions/bluebubbles/src/*|extensions/discord/src/*|extensions/feishu/src/*|extensions/googlechat/src/*|extensions/imessage/src/*|extensions/irc/src/*|extensions/line/src/*|extensions/matrix/src/*|extensions/mattermost/src/*|extensions/msteams/src/*|extensions/nextcloud-talk/src/*|extensions/nostr/src/*|extensions/qa-channel/src/*|extensions/qqbot/src/*|extensions/signal/src/*|extensions/slack/src/*|extensions/synology-chat/src/*|extensions/telegram/src/*|extensions/tlon/src/*|extensions/twitch/src/*|extensions/whatsapp/src/*|extensions/zalo/src/*|extensions/zalouser/src/*|src/channels/*)
|
||||
channel=true
|
||||
;;
|
||||
src/config/*)
|
||||
config=true
|
||||
;;
|
||||
src/gateway/protocol/*secret*.ts|src/gateway/server-methods/secrets*.ts)
|
||||
core_auth_secrets=true
|
||||
gateway=true
|
||||
;;
|
||||
src/agents/*auth*.ts|src/agents/auth-health*.ts|src/agents/auth-profiles|src/agents/auth-profiles/*|src/agents/bash-tools.exec-host-shared.ts|src/agents/sandbox|src/agents/sandbox.ts|src/agents/sandbox-*.ts|src/agents/sandbox/*|src/cron/service/jobs.ts|src/cron/stagger.ts|src/gateway/*auth*.ts|src/gateway/*secret*.ts|src/gateway/resolve-configured-secret-input-string*.ts|src/gateway/security-path*.ts|src/infra/secret-file*.ts|src/secrets/*|src/security/*)
|
||||
core_auth_secrets=true
|
||||
;;
|
||||
src/gateway/method-scopes.ts|src/gateway/protocol/*|src/gateway/server-methods/*|src/gateway/server-methods.ts|src/gateway/server-methods-list.ts)
|
||||
gateway=true
|
||||
;;
|
||||
packages/memory-host-sdk/*|src/commands/doctor-cron-dreaming-payload-migration.ts|src/commands/doctor-memory-search.ts|src/gateway/server-startup-memory.ts|src/memory/*|src/memory-host-sdk/*)
|
||||
memory=true
|
||||
;;
|
||||
src/infra/outbound/base-session-key.ts|src/infra/outbound/delivery-queue*.ts|src/infra/outbound/outbound-session.ts|src/infra/outbound/session-binding*.ts|src/infra/outbound/session-context.ts|src/infra/outbound/targets-session.ts)
|
||||
mcp_process=true
|
||||
session_diagnostics=true
|
||||
;;
|
||||
src/infra/outbound/*|src/mcp/*|src/process/*)
|
||||
mcp_process=true
|
||||
;;
|
||||
src/plugin-sdk/inbound-envelope.ts|src/plugin-sdk/inbound-reply-dispatch.ts|src/plugin-sdk/reply-*.ts|src/plugin-sdk/channel-reply-*.ts|src/plugin-sdk/delivery-queue-runtime.ts|src/plugin-sdk/outbound-runtime.ts|src/plugin-sdk/outbound-send-deps.ts|src/plugin-sdk/model-session-runtime.ts|src/plugin-sdk/session-*.ts|src/plugin-sdk/thread-bindings-runtime.ts|src/plugin-sdk/thread-bindings-session-runtime.ts|src/plugin-sdk/conversation-binding-runtime.ts)
|
||||
plugin=true
|
||||
plugin_sdk_package=true
|
||||
plugin_sdk_reply=true
|
||||
;;
|
||||
src/plugin-sdk/memory-*.ts|src/plugin-sdk/memory-core-host-*.ts)
|
||||
memory=true
|
||||
plugin=true
|
||||
plugin_sdk_package=true
|
||||
;;
|
||||
src/plugin-sdk/*)
|
||||
plugin=true
|
||||
plugin_sdk_package=true
|
||||
;;
|
||||
src/plugins/provider-contract-public-artifacts.ts|src/plugins/provider-public-artifacts.ts|src/plugins/web-provider-public-artifacts*.ts)
|
||||
plugin=true
|
||||
provider=true
|
||||
;;
|
||||
src/plugins/memory-embedding-provider*.ts|src/plugins/memory-embedding-providers*.ts)
|
||||
memory=true
|
||||
provider=true
|
||||
;;
|
||||
src/plugins/memory-*.ts)
|
||||
memory=true
|
||||
;;
|
||||
src/model-catalog/*|src/plugins/*provider*.ts|src/plugins/capability-provider-runtime.ts|src/plugins/compaction-provider.ts|src/plugins/memory-embedding-provider*.ts|src/plugins/memory-embedding-providers*.ts|src/plugins/migration-provider-runtime.ts|src/plugins/synthetic-auth.runtime.ts|src/plugins/web-fetch-providers*.ts|src/plugins/web-search-providers*.ts)
|
||||
provider=true
|
||||
;;
|
||||
src/plugins/activation-planner.ts|src/plugins/api-builder.ts|src/plugins/bundled-*.ts|src/plugins/captured-registration.ts|src/plugins/config-*.ts|src/plugins/discovery.ts|src/plugins/effective-plugin-ids.ts|src/plugins/externalized-bundled-plugins.ts|src/plugins/installed-plugin-index*.ts|src/plugins/loader*.ts|src/plugins/manifest*.ts|src/plugins/module-export.ts|src/plugins/package-entrypoints.ts|src/plugins/plugin-registry*.ts|src/plugins/public-surface*.ts|src/plugins/registry.ts|src/plugins/registry-types.ts|src/plugins/runtime|src/plugins/runtime/*|src/plugins/runtime-state.ts|src/plugins/runtime.ts|src/plugins/sdk-alias.ts|src/plugins/source-loader.ts|src/plugins/types.ts|src/plugins/validation-diagnostics.ts)
|
||||
plugin=true
|
||||
;;
|
||||
packages/plugin-package-contract/*|packages/plugin-sdk/*)
|
||||
plugin_sdk_package=true
|
||||
;;
|
||||
esac
|
||||
done < <(gh api --paginate "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[].filename')
|
||||
fi
|
||||
|
||||
{
|
||||
echo "agent=${agent}"
|
||||
echo "channel=${channel}"
|
||||
echo "config=${config}"
|
||||
echo "core_auth_secrets=${core_auth_secrets}"
|
||||
echo "gateway=${gateway}"
|
||||
echo "memory=${memory}"
|
||||
echo "mcp_process=${mcp_process}"
|
||||
echo "plugin=${plugin}"
|
||||
echo "plugin_sdk_package=${plugin_sdk_package}"
|
||||
echo "plugin_sdk_reply=${plugin_sdk_reply}"
|
||||
echo "provider=${provider}"
|
||||
echo "session_diagnostics=${session_diagnostics}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
|
||||
core-auth-secrets:
|
||||
name: Critical Quality (core-auth-secrets)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.core_auth_secrets == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'core-auth-secrets') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -323,8 +52,7 @@ jobs:
|
||||
|
||||
config-boundary:
|
||||
name: Critical Quality (config-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.config == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'config-boundary') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -346,8 +74,7 @@ jobs:
|
||||
|
||||
gateway-runtime-boundary:
|
||||
name: Critical Quality (gateway-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.gateway == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'gateway-runtime-boundary') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -369,8 +96,7 @@ jobs:
|
||||
|
||||
channel-runtime-boundary:
|
||||
name: Critical Quality (channel-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.channel == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'channel-runtime-boundary') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -392,8 +118,7 @@ jobs:
|
||||
|
||||
agent-runtime-boundary:
|
||||
name: Critical Quality (agent-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.agent == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'agent-runtime-boundary') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -415,8 +140,7 @@ jobs:
|
||||
|
||||
mcp-process-runtime-boundary:
|
||||
name: Critical Quality (mcp-process-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.mcp_process == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'mcp-process-runtime-boundary') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -438,8 +162,7 @@ jobs:
|
||||
|
||||
memory-runtime-boundary:
|
||||
name: Critical Quality (memory-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.memory == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'memory-runtime-boundary') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -461,8 +184,7 @@ jobs:
|
||||
|
||||
session-diagnostics-boundary:
|
||||
name: Critical Quality (session-diagnostics-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.session_diagnostics == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'session-diagnostics-boundary') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'session-diagnostics-boundary' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -482,55 +204,9 @@ jobs:
|
||||
with:
|
||||
category: "/codeql-critical-quality/session-diagnostics-boundary"
|
||||
|
||||
plugin-sdk-reply-runtime:
|
||||
name: Critical Quality (plugin-sdk-reply-runtime)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.plugin_sdk_reply == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-reply-runtime') }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
config-file: ./.github/codeql/codeql-plugin-sdk-reply-runtime-critical-quality.yml
|
||||
|
||||
- name: Analyze
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||
with:
|
||||
category: "/codeql-critical-quality/plugin-sdk-reply-runtime"
|
||||
|
||||
provider-runtime-boundary:
|
||||
name: Critical Quality (provider-runtime-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.provider == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'provider-runtime-boundary') }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
config-file: ./.github/codeql/codeql-provider-runtime-boundary-critical-quality.yml
|
||||
|
||||
- name: Analyze
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||
with:
|
||||
category: "/codeql-critical-quality/provider-runtime-boundary"
|
||||
|
||||
ui-control-plane:
|
||||
name: Critical Quality (ui-control-plane)
|
||||
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -552,7 +228,7 @@ jobs:
|
||||
|
||||
web-media-runtime-boundary:
|
||||
name: Critical Quality (web-media-runtime-boundary)
|
||||
if: ${{ github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -574,8 +250,7 @@ jobs:
|
||||
|
||||
plugin-boundary:
|
||||
name: Critical Quality (plugin-boundary)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.plugin == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-boundary') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
@@ -597,8 +272,7 @@ jobs:
|
||||
|
||||
plugin-sdk-package-contract:
|
||||
name: Critical Quality (plugin-sdk-package-contract)
|
||||
needs: quality-shards
|
||||
if: ${{ needs.quality-shards.outputs.plugin_sdk_package == 'true' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name == 'pull_request' || github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-package-contract') }}
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'plugin-sdk-package-contract' }}
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
|
||||
20
.github/workflows/codeql.yml
vendored
20
.github/workflows/codeql.yml
vendored
@@ -11,20 +11,12 @@ on:
|
||||
options:
|
||||
- all
|
||||
- security
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- ".github/actions/**"
|
||||
- ".github/codeql/**"
|
||||
- ".github/workflows/**"
|
||||
- "packages/**"
|
||||
- "src/**"
|
||||
schedule:
|
||||
- cron: "0 6 * * *"
|
||||
|
||||
concurrency:
|
||||
group: codeql-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.event_name == 'pull_request' && github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
group: codeql-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.sha }}
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
||||
@@ -35,9 +27,9 @@ permissions:
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
security-high:
|
||||
name: Security High (${{ matrix.category }})
|
||||
if: ${{ (github.event_name != 'pull_request' || !github.event.pull_request.draft) && (github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'security') }}
|
||||
critical-security:
|
||||
name: Critical Security (${{ matrix.category }})
|
||||
if: ${{ github.event_name != 'workflow_dispatch' || inputs.profile == 'all' || inputs.profile == 'security' }}
|
||||
runs-on: ${{ matrix.runs_on }}
|
||||
timeout-minutes: ${{ matrix.timeout_minutes }}
|
||||
strategy:
|
||||
@@ -89,4 +81,4 @@ jobs:
|
||||
- name: Analyze
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||
with:
|
||||
category: "/codeql-security-high/${{ matrix.category }}"
|
||||
category: "/codeql-critical-security/${{ matrix.category }}"
|
||||
|
||||
14
.github/workflows/install-smoke.yml
vendored
14
.github/workflows/install-smoke.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
||||
run_bun_global_install_smoke: ${{ steps.manifest.outputs.run_bun_global_install_smoke }}
|
||||
target_sha: ${{ steps.manifest.outputs.target_sha }}
|
||||
dockerfile_image: ${{ steps.manifest.outputs.dockerfile_image }}
|
||||
dockerfile_cache_scope: ${{ steps.manifest.outputs.dockerfile_cache_scope }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -80,6 +81,7 @@ jobs:
|
||||
target_sha="$(git rev-parse HEAD)"
|
||||
owner="$(printf '%s' "${GITHUB_REPOSITORY_OWNER:-openclaw}" | tr '[:upper:]' '[:lower:]')"
|
||||
dockerfile_image="ghcr.io/${owner}/openclaw-dockerfile-smoke:${target_sha}"
|
||||
dockerfile_cache_scope="openclaw-dockerfile-smoke"
|
||||
if [ "$event_name" = "schedule" ]; then
|
||||
run_bun_global_install_smoke=true
|
||||
elif [ "$event_name" = "workflow_dispatch" ] || [ "$event_name" = "workflow_call" ]; then
|
||||
@@ -95,6 +97,7 @@ jobs:
|
||||
echo "run_bun_global_install_smoke=$run_bun_global_install_smoke"
|
||||
echo "target_sha=$target_sha"
|
||||
echo "dockerfile_image=$dockerfile_image"
|
||||
echo "dockerfile_cache_scope=$dockerfile_cache_scope"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
install-smoke-fast:
|
||||
@@ -111,7 +114,7 @@ jobs:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Set up Blacksmith Docker Builder
|
||||
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
with:
|
||||
max-cache-size-mb: 800000
|
||||
|
||||
@@ -242,7 +245,7 @@ jobs:
|
||||
|
||||
- name: Set up Blacksmith Docker Builder
|
||||
if: steps.existing.outputs.exists != 'true'
|
||||
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
with:
|
||||
max-cache-size-mb: 800000
|
||||
|
||||
@@ -251,11 +254,14 @@ jobs:
|
||||
- name: Build and push root Dockerfile smoke image
|
||||
if: steps.existing.outputs.exists != 'true'
|
||||
env:
|
||||
CACHE_SCOPE: ${{ needs.preflight.outputs.dockerfile_cache_scope }}
|
||||
IMAGE_REF: ${{ needs.preflight.outputs.dockerfile_image }}
|
||||
run: |
|
||||
timeout 45m docker buildx build \
|
||||
--progress=plain \
|
||||
--push \
|
||||
--cache-from "type=gha,scope=${CACHE_SCOPE}" \
|
||||
--cache-to "type=gha,scope=${CACHE_SCOPE},mode=max" \
|
||||
--build-arg OPENCLAW_EXTENSIONS=matrix \
|
||||
-t "$IMAGE_REF" \
|
||||
-f ./Dockerfile \
|
||||
@@ -408,7 +414,7 @@ jobs:
|
||||
run: timeout 300s docker pull "$IMAGE_REF"
|
||||
|
||||
- name: Set up Blacksmith Docker Builder
|
||||
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
with:
|
||||
max-cache-size-mb: 800000
|
||||
|
||||
@@ -501,7 +507,7 @@ jobs:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Set up Blacksmith Docker Builder
|
||||
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
with:
|
||||
max-cache-size-mb: 800000
|
||||
|
||||
|
||||
@@ -36,12 +36,12 @@ jobs:
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Set up Blacksmith Docker Builder
|
||||
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
with:
|
||||
max-cache-size-mb: 800000
|
||||
|
||||
- name: Build and push live media runner image
|
||||
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
|
||||
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
|
||||
with:
|
||||
context: .github/images/live-media-runner
|
||||
file: .github/images/live-media-runner/Dockerfile
|
||||
|
||||
4
.github/workflows/npm-telegram-beta-e2e.yml
vendored
4
.github/workflows/npm-telegram-beta-e2e.yml
vendored
@@ -105,12 +105,12 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Set up Blacksmith Docker Builder
|
||||
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
with:
|
||||
max-cache-size-mb: 800000
|
||||
|
||||
- name: Build Docker E2E image
|
||||
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
|
||||
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./scripts/e2e/Dockerfile
|
||||
|
||||
@@ -1359,13 +1359,13 @@ jobs:
|
||||
|
||||
- name: Setup Docker builder
|
||||
if: steps.image_exists.outputs.needs_build == '1'
|
||||
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
with:
|
||||
max-cache-size-mb: 800000
|
||||
|
||||
- name: Build and push bare Docker E2E image
|
||||
if: steps.plan.outputs.needs_bare_image == '1' && steps.image_exists.outputs.bare_exists != '1'
|
||||
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
|
||||
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./scripts/e2e/Dockerfile
|
||||
@@ -1378,7 +1378,7 @@ jobs:
|
||||
|
||||
- name: Build and push functional Docker E2E image
|
||||
if: steps.plan.outputs.needs_functional_image == '1' && steps.image_exists.outputs.functional_exists != '1'
|
||||
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
|
||||
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./scripts/e2e/Dockerfile
|
||||
@@ -1444,13 +1444,13 @@ jobs:
|
||||
|
||||
- name: Setup Docker builder
|
||||
if: steps.image_exists.outputs.exists != '1'
|
||||
uses: useblacksmith/setup-docker-builder@722e97d12b1d06a961800dd6c05d79d951ad3c80 # v1
|
||||
uses: useblacksmith/setup-docker-builder@ac083cc84672d01c60d5e8561d0a939b697de542 # v1
|
||||
with:
|
||||
max-cache-size-mb: 800000
|
||||
|
||||
- name: Build and push shared live-test image
|
||||
if: steps.image_exists.outputs.exists != '1'
|
||||
uses: useblacksmith/build-push-action@fb9e3e6a9299c78462bfadd0d93352c316adc9b8 # v2
|
||||
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -137,7 +137,6 @@ USER.md
|
||||
.agent/*.json
|
||||
!.agent/workflows/
|
||||
/local/
|
||||
/client_secret_*.json
|
||||
package-lock.json
|
||||
.claude/
|
||||
.agent/
|
||||
|
||||
@@ -142,7 +142,6 @@ Telegraph style. Root rules only. Read scoped `AGENTS.md` before subtree work.
|
||||
## Docs / Changelog
|
||||
|
||||
- Docs change with behavior/API. Use docs list/read_when hints; docs links per `docs/AGENTS.md`.
|
||||
- Docs final answers: when doc files changed, end with the relevant full `https://docs.openclaw.ai/...` URL(s).
|
||||
- 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). Never add `Thanks @codex`, `Thanks @openclaw`, or `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.
|
||||
|
||||
114
CHANGELOG.md
114
CHANGELOG.md
@@ -2,101 +2,33 @@
|
||||
|
||||
Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.4.29
|
||||
|
||||
### Highlights
|
||||
|
||||
- Messaging and automation get active-run steering by default, visible-reply enforcement, spawned subagent routing metadata, and opt-in follow-up commitments for heartbeat-delivered reminders. Thanks @vincentkoc, @scoootscooob, @samzong, and @vignesh07.
|
||||
- Memory grows into a people-aware wiki with provenance views, per-conversation Active Memory filters, partial recall on timeout, and bounded REM preview diagnostics. Thanks @vincentkoc, @quengh, @joeykrug, and @samzong.
|
||||
- Provider/model coverage expands with NVIDIA onboarding/catalogs plus faster manifest-backed model/auth paths, Bedrock Opus 4.7 thinking parity, and safer Codex/OpenAI-compatible replay and streaming behavior. Thanks @eleqtrizit, @shakkernerd, @prasad-yashdeep, @woodhouse-bot, and @LyHug.
|
||||
- Gateway and packaged-plugin reliability focuses on slow-host startup, reusable model catalogs, event-loop readiness diagnostics, runtime-dependency repair, stale-session recovery, and version-scoped update caches. Thanks @lpendeavors, @DerFlash, @vincentkoc, @pashpashpash, and @jhsmith409.
|
||||
- Channel fixes cluster around Slack Block Kit limits, Telegram proxy/webhook/polling/send resilience, Discord startup/rate-limit handling, WhatsApp delivery/liveness, and Microsoft Teams/Matrix/Feishu edge cases. Thanks @slackapi, @SymbolStar, @djgeorg3, @TinyTb, @dseravalli, @nklock, and @alex-xuweilong.
|
||||
- Security and operations add OpenGrep scanning, sharper GHSA triage policy, safer exec/pairing/owner-scope handling, Docker/onboarding automation, and web-fetch IPv6 ULA opt-in for trusted proxy stacks. Thanks @jesse-merhi, @pgondhi987, @mmaps, @jinjimz, and @jeffrey701.
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
|
||||
- Agents/commitments: add opt-in inferred follow-up commitments with hidden batched extraction, per-agent/per-channel scoping, heartbeat delivery, CLI management, a simple `commitments.enabled`/`commitments.maxPerDay` config, and heartbeat-interval due-time clamping so magical check-ins do not echo immediately. (#74189) Thanks @vignesh07.
|
||||
- Messages/queue: make `steer` drain all pending Pi steering messages at the next model boundary, keep legacy one-at-a-time steering as `queue`, and add a dedicated steering queue docs page. Thanks @vincentkoc.
|
||||
- Dependencies: refresh workspace runtime, plugin, and tooling packages, including ACP, Pi, AWS SDK, TypeBox, pnpm, oxlint, oxfmt, jsdom, pdfjs, ciao, and tokenjuice, while keeping patched ACP behavior and lint gates current. Thanks @mariozechner.
|
||||
- Messages/queue: default active-run queueing to `steer` with a 500ms followup fallback debounce, and document the queue modes, precedence, and drop policies on the command queue page. Thanks @vincentkoc.
|
||||
- Messages: add global `messages.visibleReplies` so operators can require visible output to go through `message(action=send)` for any source chat, while `messages.groupChat.visibleReplies` stays available as the group/channel override. Thanks @scoootscooob.
|
||||
- Gateway/events: surface `spawnedBy` on subagent chat and agent broadcast payloads so clients can route child session events without an extra session lookup. (#63244) Thanks @samzong.
|
||||
- Memory/wiki: add agent-facing people wiki metadata, canonical aliases, person cards, relationship graphs, privacy/provenance reports, evidence-kind drilldown, and search modes for person lookup, question routing, source evidence, and raw claims. Thanks @vincentkoc.
|
||||
- Active Memory: add optional per-conversation `allowedChatIds` and `deniedChatIds` filters so operators can enable recall only for selected direct, group, or channel conversations while keeping broad sessions skipped. (#67977) Thanks @quengh.
|
||||
- Active Memory: return bounded partial recall summaries when the hidden memory sub-agent times out, including the default temporary-transcript path, so useful recovered context is not discarded. (#73219) Thanks @joeykrug.
|
||||
- Gateway/memory: add a read-only `doctor.memory.remHarness` RPC so operator clients can preview bounded REM dreaming output without running mutation paths. (#66673) Thanks @samzong.
|
||||
- Providers/NVIDIA: add the NVIDIA provider with API-key onboarding, setup docs, static catalog metadata, and literal model-ref picker support so NVIDIA hosted models can be selected with their provider prefix intact. (#71204) Thanks @eleqtrizit.
|
||||
- Models: suppress explicitly configured openai-codex/gpt-5.4-mini inline entries so a stale models config written by `openclaw doctor --fix` cannot bypass the manifest capability block and cause repeated assistant-turn failures when the runtime switches to that model on ChatGPT-backed Codex accounts. Conditional suppressions (e.g. qwen Coding Plan endpoint guards) remain bypassable by explicit user configuration. (#74451) Thanks @0xCyda, @hclsys, and @Marvae.
|
||||
- Added SQLite-backed plugin state store (`api.runtime.state.openKeyedStore`) for restart-safe keyed registries with TTL, eviction, and automatic plugin isolation. Thanks @amknight.
|
||||
- Memory/wiki: add agent-facing people wiki metadata, canonical aliases, person cards, relationship graphs, privacy/provenance reports, evidence-kind drilldown, and search modes for person lookup, question routing, source evidence, and raw claims. Thanks @vincentkoc.
|
||||
- Messages: add global `messages.visibleReplies` so operators can require visible output to go through `message(action=send)` for any source chat, while `messages.groupChat.visibleReplies` stays available as the group/channel override. Thanks @scoootscooob.
|
||||
- Gateway/dev: run `pnpm gateway:watch` through a named tmux session by default, with `gateway:watch:raw` and `OPENCLAW_GATEWAY_WATCH_TMUX=0` for foreground mode, so repeated starts respawn an inspectable watcher without trapping the invoking agent shell. Thanks @vincentkoc.
|
||||
- Plugin SDK: mark remaining legacy alias exports and diffs tool/config aliases with deprecation metadata, and add a guard so future legacy alias comments require `@deprecated` tags. Thanks @vincentkoc.
|
||||
- CLI/QR/dependencies: internalize small terminal progress and QR wrapper helpers while keeping the real QR encoder dependency direct, reducing the default runtime dependency graph without changing QR output behavior. Thanks @vincentkoc.
|
||||
- Dependencies: refresh workspace runtime, plugin, and tooling packages, including ACP, Pi, AWS SDK, TypeBox, pnpm, oxlint, oxfmt, jsdom, pdfjs, ciao, and tokenjuice, while keeping patched ACP behavior and lint gates current. Thanks @mariozechner.
|
||||
- Gateway/dev: run `pnpm gateway:watch` through a named tmux session by default, with `gateway:watch:raw` and `OPENCLAW_GATEWAY_WATCH_TMUX=0` for foreground mode, so repeated starts respawn an inspectable watcher without trapping the invoking agent shell. Thanks @vincentkoc.
|
||||
- Gateway/diagnostics: emit an opt-in startup diagnostics timeline that records gateway lifecycle and plugin-load phases behind a config flag, so slow-start diagnosis no longer requires bespoke instrumentation. Thanks @shakkernerd.
|
||||
- Control UI/i18n: extend the locale registry with new Persian (fa), Dutch (nl), Vietnamese (vi), Italian (it), Arabic (ar), and Thai (th) entries and ship `fa`, `nl`, `vi`, and `zh-TW` docs glossaries, so the docs translation pipeline and the Control UI language picker stay aligned across surfaces. Thanks @vincentkoc.
|
||||
- Channels: add Yuanbao channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.
|
||||
- Active Memory: add optional per-conversation `allowedChatIds` and `deniedChatIds` filters so operators can enable recall only for selected direct, group, or channel conversations while keeping broad sessions skipped. (#67977) Thanks @quengh.
|
||||
- Added SQLite-backed plugin state store (`api.runtime.state.openKeyedStore`) for restart-safe keyed registries with TTL, eviction, and automatic plugin isolation. Thanks @amknight.
|
||||
- Active Memory: return bounded partial recall summaries when the hidden memory sub-agent times out, including the default temporary-transcript path, so useful recovered context is not discarded. (#73219) Thanks @joeykrug.
|
||||
- Docker setup: add `OPENCLAW_SKIP_ONBOARDING` so automated Docker installs can skip the interactive onboarding step while still applying gateway defaults. (#55518) Thanks @jinjimz.
|
||||
- Gateway/memory: add a read-only `doctor.memory.remHarness` RPC so operator clients can preview bounded REM dreaming output without running mutation paths. (#66673) Thanks @samzong.
|
||||
- Gateway/events: surface `spawnedBy` on subagent chat and agent broadcast payloads so clients can route child session events without an extra session lookup. (#63244) Thanks @samzong.
|
||||
- Security policy: classify media/base64 decode and format-conversion overhead after configured acceptance limits as performance-only for GHSA triage unless a report demonstrates a limit bypass, crash, exhaustion, data exposure, or another boundary bypass. (#74311)
|
||||
- Security/OpenGrep: add a precise OpenGrep rulepack, source-rule compiler, provenance metadata check, and PR/full scan workflows that validate first-party code and rulepack-only changes while uploading SARIF to GitHub Code Scanning. (#69483) Thanks @jesse-merhi.
|
||||
- Control UI/i18n: extend the locale registry with new Persian (fa), Dutch (nl), Vietnamese (vi), Italian (it), Arabic (ar), and Thai (th) entries and ship `fa`, `nl`, `vi`, and `zh-TW` docs glossaries, so the docs translation pipeline and the Control UI language picker stay aligned across surfaces. Thanks @vincentkoc.
|
||||
- Gateway/diagnostics: emit an opt-in startup diagnostics timeline that records gateway lifecycle and plugin-load phases behind a config flag, so slow-start diagnosis no longer requires bespoke instrumentation. Thanks @shakkernerd.
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI/agents/status: keep `openclaw agents`, text `agents list`, and plain text `status` on read-only metadata paths so human output no longer preloads plugin runtimes or live channel scans before printing. Fixes #74195. Thanks @NianJiuZst.
|
||||
- Media: treat legacy Word/OLE attachments with `application/msword` or `application/x-cfb` MIME as binary so printable-looking `.doc` files are not embedded into prompts as text. Fixes #54176; carries forward #54380. Thanks @andyliu.
|
||||
- Config: accept documented `browser.tabCleanup` keys in strict root config validation, so configured tab cleanup no longer fails before runtime reads it. Fixes #74577. Thanks @lonexreb and @ezdlp.
|
||||
- Cron: validate disabled job schedule edits before persisting updates, so invalid cron changes no longer partially mutate stored jobs. Fixes #74459. Thanks @yfge.
|
||||
- Channels/status: keep Telegram, Slack, and Google Chat read-only allowlist/default-target accessors on config-only paths, so status and channel summaries do not resolve SecretRef-backed runtime credentials. Thanks @eusine.
|
||||
- Active Memory: clarify the deprecated `modelFallbackPolicy` warning and config help so `modelFallback` is described as a chain-resolution last resort, not runtime failover. (#74602) Thanks @jeffrey701.
|
||||
- Channels/Discord: keep read-only allowlist/default-target accessors from resolving SecretRef-backed bot tokens, so status and channel summaries no longer fail when tokens are only available in gateway runtime. (#74737) Thanks @eusine.
|
||||
- Gateway/sessions: align session abort wait semantics across `chat`, `agent`, and `sessions` server methods so abort RPCs return after the targeted sessions actually halt instead of resolving early while runs are still draining. (#74751) Thanks @BunsDev.
|
||||
- Agents/output: drop copied inbound metadata-only assistant replay turns before provider replay instead of synthesizing a placeholder, so Telegram and other channels cannot receive `[assistant copied inbound metadata omitted]` as model output. Fixes #74745. Thanks @adamwdear and @Marvae.
|
||||
- Doctor/memory: suppress skipped embedding-readiness warnings for key-optional providers such as Ollama and LM Studio while preserving timeout and not-ready diagnostics. Fixes #74608 and #73882. Thanks @hclsys.
|
||||
- Channels/groups: preserve observe-only turn suppression for prepared dispatch paths and restore deprecated channel turn runtime aliases, so passive observer/group flows stay silent while older plugins keep compiling. Thanks @vincentkoc.
|
||||
- Feishu: skip empty-text messages (e.g. `{"text":""}`) that carry no media, so no blank user turn is written to the session and downstream LLM providers cannot reject the request with "messages must not be empty". (#74634) Thanks @xdengli and @hclsys.
|
||||
- Feishu/Bitable: clean up newly created placeholder rows whose fields contain only default empty values while preserving meaningful link, attachment, user, number, boolean, and location values during create-app cleanup. (#73920) Carries forward #40602. Thanks @boat2moon.
|
||||
- macOS app: keep attach-only mode and the Debug Settings launchd toggle marker-only, so launching with `--attach-only`/`--no-launchd` no longer uninstalls the Gateway LaunchAgent or drops active sessions. (#72174) Thanks @DolencLuka.
|
||||
- Plugin SDK: restore the deprecated `plugin-sdk/zalouser` command-auth facade so published Lark/Zalo plugins that import it load on current hosts. Fixes #74702. Thanks @Goron01.
|
||||
- Plugins/runtime-deps: include bundled provider plugins when `models.providers`, auth profiles, agent defaults, or subagent model refs configure that provider, while keeping inactive default-enabled provider plugins out of doctor repair. Refs #74307. Thanks @Skeptomenos.
|
||||
- Plugins/runtime: resolve relative plugin `api.resolvePath` inputs against the plugin root instead of the host working directory, while keeping absolute and home paths user-resolved. Fixes #74718. Thanks @jimdawdy-hub.
|
||||
- Plugins/runtime-deps: refresh mirrored root chunks through a temporary file before replacing the active copy, so failed refreshes do not delete chunks that running plugin imports still need. Thanks @shakkernerd.
|
||||
- Plugins/runtime-deps: prefer `require` conditional exports when building staged dependency aliases, so CommonJS-only plugin runtime deps such as `ws` do not resolve to ESM wrappers under Jiti. Fixes #74547. Thanks @aderius.
|
||||
- Bonjour/Gateway: cap flapping advertiser restarts in a sliding window, so mDNS probing/name-conflict loops disable discovery instead of churning indefinitely on constrained hosts. Refs #74209 and #74242. Thanks @ndj888 and @Sanjays2402.
|
||||
- Plugins/runtime-deps: verify staged package entry files before reusing mirrored runtime roots, so browser-control repairs incomplete `ajv`/MCP SDK installs after update instead of failing after restart on a missing `ajv/dist/ajv.js`. Refs #74630. Thanks @spickeringlr.
|
||||
- Heartbeat: resolve `responsePrefix` template variables with the selected provider, model, and thinking context before delivering alerts or suppressing prefixed `HEARTBEAT_OK` replies. Fixes #43064; repairs #43065; supersedes #46858. Thanks @yweiii and @JunJD.
|
||||
- Memory/LanceDB: show full memory UUIDs in the `memory_forget` candidate list so agents can pass the displayed ID back to targeted deletion without hitting the full-UUID validator. (#66913) Thanks @amittell.
|
||||
- File-transfer plugin: require canonical read-path preflight authorization for `file.fetch`, fail closed when `dir.fetch` preflight entries are missing, absolute, or traversing, and recheck returned archive entries before handing archive bytes to callers. Carries forward #74134. Thanks @omarshahine.
|
||||
- Channels/Feishu: retry file-typed iOS video resource downloads as `media` after a Feishu/Lark HTTP 502 and preserve the original 502 when the fallback also fails. Fixes #49855; carries forward #50164 and #73986. Thanks @alex-xuweilong.
|
||||
- Providers/Amazon Bedrock: expose the full Claude Opus 4.7 thinking profile (`xhigh`, `adaptive`, and `max`) for Bedrock model refs, while keeping Opus/Sonnet 4.6 on adaptive-by-default, so `/think` menus and validation match the Anthropic transport behavior. Fixes #74701. Thanks @prasad-yashdeep, @sparkleHazard, @Sanjays2402, and @hclsys.
|
||||
- Plugins/tokenjuice: compile the bundled plugin against tokenjuice 0.7.0's published OpenClaw host types instead of a local compatibility shim, so package contract drift fails in OpenClaw validation before release. Thanks @vincentkoc.
|
||||
- OAuth/secrets: ignore root-level Google OAuth `client_secret_*.json` downloads so local client-secret files do not appear as commit candidates. (#74689) Thanks @jeongdulee.
|
||||
- Memory: mirror `sqlite-vec` into packaged bundled-plugin runtime deps for the default memory plugin, so builtin vector search does not lose its SQLite extension after upgrading to 2026.4.27. Fixes #74692. Thanks @mozi1924.
|
||||
- Gateway/startup: bound local discovery advertisement during startup, so a stuck discovery plugin can no longer keep the Gateway from reaching ready. Fixes #73865; refs #74630 and #74633. Thanks @lpendeavors, @moltar-bot, and @Saboor711.
|
||||
- Gateway/models: serve the last successful model catalog while stale reloads refresh in the background, so Gateway control-plane and OpenAI-compatible requests no longer block behind model-provider rediscovery after model config changes. Refs #74135, #74630, and #74633. Thanks @DerFlash, @moltar-bot, and @Saboor711.
|
||||
- CLI/status: resolve read-only channel setup runtime fallback from the packaged OpenClaw dist root, so `status --all`, `status --deep`, channel, and doctor paths do not crash when an external channel plugin needs setup metadata. Fixes #74693. Thanks @giangthb.
|
||||
- SDK/events: keep per-run SDK event streams from surfacing duplicate raw chat projection frames, while normalizing chat-only projection frames and preserving raw access through `rawEvents`. Refs #74704. Thanks @BunsDev.
|
||||
- Google Meet: block managed Chrome intro/test speech until browser health proves the participant is in-call, and expose `speechReady` diagnostics so login, admission, permission, and audio-bridge blockers no longer look like successful speech. Refs #72478. Thanks @DougButdorf.
|
||||
- Slack/commands: keep native command argument menus on select controls for encoded choice values up to Slack's option limit and truncate fallback button labels to Slack's button-text limit, so long valid choices no longer render invalid Slack blocks. Thanks @slackapi.
|
||||
- Agents/Codex: flush accepted debounced steering messages before normal app-server turn cleanup, so inbound follow-ups acknowledged as queued are not dropped when the turn completes before the debounce fires. Thanks @vincentkoc.
|
||||
- Slack/interactive replies: keep rendered buttons and selects within Slack Block Kit value and count limits, and align command argument select values with Slack's option limit, so overlong agent-authored choices no longer make Slack reject the whole block payload. Thanks @slackapi.
|
||||
- Slack/interactive replies: drop overlong Block Kit button URLs while preserving valid callback values, so malformed link buttons no longer make Slack reject the whole interactive reply. Thanks @slackapi.
|
||||
- Slack/commands: truncate native command argument-menu confirmation text to Slack's dialog limit, so long plugin arg names no longer make fallback buttons render invalid Block Kit payloads. Thanks @slackapi.
|
||||
- Slack/exec approvals: cap native approval metadata context to Slack's element and text limits, so large approval details no longer make Slack reject the approval card. Thanks @slackapi.
|
||||
- Slack/exec approvals: cap native approval update fallback text to Slack's message limit while preserving the rendered approval blocks, so long commands no longer make resolved or expired approval cards stay stale after `chat.update` rejects `msg_too_long`. Thanks @slackapi.
|
||||
- Slack/commands: cap native command argument-menu fallback rows to Slack's message block limit, so large plugin choice lists no longer make Slack reject the generated menu. Thanks @slackapi.
|
||||
- Slack/commands: drop fallback command argument buttons whose encoded values exceed Slack's button-value limit, so one oversized plugin choice no longer makes Slack reject the whole menu. Thanks @slackapi.
|
||||
- Slack/messages: merge message-tool presentation and interactive blocks on Slack sends, so buttons and selects are no longer dropped when a structured message body is also present. Thanks @slackapi.
|
||||
- Slack/messages: cap Block Kit fallback text to Slack's send limit while preserving the rendered blocks, so long context fallbacks no longer make rich Slack messages fail with `msg_too_long`. Thanks @slackapi.
|
||||
- Slack/messages: cap Block Kit fallback text on message edits while preserving the rendered blocks, so long context fallbacks no longer make Slack reject `chat.update` calls with `msg_too_long`. Thanks @slackapi.
|
||||
- Channels/WhatsApp: require Baileys outbound message ids before marking auto-replies delivered, so transcript text and ack reactions no longer make failed group replies look sent. Fixes #49225. Thanks @TinyTb.
|
||||
- CLI/update: scope packaged Node compile caches by OpenClaw version and install metadata, so global installs no longer reuse stale compiled chunks after package updates. Thanks @pashpashpash.
|
||||
- Channels/Voice call: keep pre-auth webhook in-flight limiting active when socket remote address metadata is missing, so slow-body requests from stripped-IP proxy paths still share the fallback bucket. (#74453) Thanks @davidangularme.
|
||||
- Plugin SDK/testing: lazy-load TypeScript from the plugin test-contract runtime and add release checks for critical SDK contract entrypoint imports and bundle size, so published packages fail preflight before shipping ESM-incompatible or oversized contract helpers. Thanks @vincentkoc.
|
||||
- Channels/Microsoft Teams: treat configured `19:...@thread.tacv2` and legacy `19:...@thread.skype` team/channel IDs as already resolved during startup, avoiding false `channels unresolved` warnings while preserving Graph name lookup for display-name entries. Fixes #74683. Thanks @dseravalli.
|
||||
- CLI/browser: preserve parent flags while lazy-loading browser subcommands, so `openclaw browser --json open` and `openclaw browser --json tabs` keep machine-readable output after reparsing. Fixes #74574. Thanks @devintegeritsm.
|
||||
- Exec/elevated: preserve `turnSourceChannel` as `messageProvider` on approval-followup runs so `tools.elevated.allowFrom.<provider>` checks no longer fail with `provider=null` after the user approves an async elevated command. Fixes #74646. Thanks @xhd2015.
|
||||
- Plugins/runtime-deps: add `openclaw plugins deps` inspection and repair with script-free package-manager defaults shared across plugin installers, so operators can repair missing bundled runtime deps without corrupting JSON output or blocking unrelated conflict-free deps. Thanks @vincentkoc.
|
||||
- Agents/output: strip internal `[tool calls omitted]` replay placeholders from user-facing replies while preserving visible reply whitespace. Fixes #74573. Thanks @blaspat.
|
||||
- Providers/Google Vertex: route authorized_user ADC credentials through OpenClaw's REST transport so Docker installs using gcloud application-default credentials no longer crash in the Google SDK before requests are sent. Fixes #74628. Thanks @frankhal2001-design.
|
||||
- ACP/resolver: fall through to thread-bound session resolution when an explicit `--session` token cannot be resolved while preserving the bad-token diagnostic when no thread binding exists, so Discord slash commands that auto-fill the current thread ID as the positional ACP target no longer return "Unable to resolve session target" errors. Fixes #66299. Thanks @hclsys, @kindomLee, and @martingarramon.
|
||||
- Agents/sessions: emit a terminal lifecycle backstop when embedded timeout/error turns return without `agent_end`, so Gateway sessions no longer stay stuck in `running` after failover surfaces a timeout. Fixes #74607. Thanks @millerc79.
|
||||
- Gateway/diagnostics: include stuck-session reason hints and recovery skip causes in warnings, so operators can tell whether a lane is waiting on active work, queued work, or stale bookkeeping. Thanks @vincentkoc.
|
||||
- Agents/Codex: bound embedded-run cleanup, trajectory flushing, and command-lane task timeouts after runtime failures, so Discord and other chat sessions return to idle instead of staying stuck in processing. Thanks @vincentkoc.
|
||||
@@ -108,8 +40,6 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/status: honor channel-specific model context-window overrides when reporting effective context, so channel-scoped sessions reflect the active window in `openclaw status`. Thanks @HemantSudarshan.
|
||||
- Sandbox/Docker: tolerate Docker daemon unavailability when sandbox mode is off, so doctor and preflight checks no longer fail on installs that do not run the Docker daemon. Fixes #73671. Thanks @kaseonedge.
|
||||
- Control UI/mobile: persist mobile chat settings through Lit-managed state and route mobile navigation through the same view-state path so chat panel toggles survive transitions on small viewports. Thanks @BunsDev.
|
||||
- Control UI/exports: align sidebar trigger affordances across the resizable divider, mobile layout, and exported-HTML transcript template so the sidebar toggle and exported transcript sidebar render with consistent hit areas and styling. Thanks @BunsDev.
|
||||
- Control UI/chat: disable the page refresh affordance while a chat run is active so accidental refreshes do not abort an in-flight reply. Thanks @BunsDev.
|
||||
- Media: include redacted per-attempt resize failures and resolved model input capabilities in vision-pipeline errors so ARM64 image failures are diagnosable without closing the remaining routing investigation. Refs #74552. Thanks @1yihui.
|
||||
- Control UI/i18n: route zh-CN agent, debug, channel-refresh, and exec-approval copy through the locale source while preserving the English `Cron Jobs` agent tab label and the security-audit command styling. Carries forward #39692 repair context. Thanks @hepeng154833488 and @vincentkoc.
|
||||
- Auto-reply: honor explicit `silentReply.direct: "allow"` for clean empty or reasoning-only direct chat turns while keeping the default direct-chat empty-response guard conservative. Fixes #74409. Thanks @jesuskannolis.
|
||||
@@ -122,7 +52,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Ollama: keep explicit local model runs on target-provider runtime hooks when PI discovery is skipped, so one-shot Ollama calls no longer cold-load unrelated provider runtimes before streaming. Fixes #74078. Thanks @sakalaboator.
|
||||
- Slack/prompts: rely on Slack `interactiveReplies` guidance instead of generic `inlineButtons` config hints so enabled Slack button directives are not contradicted. Fixes #46647. Thanks @jeremykoerber.
|
||||
- Slack/reactions: treat duplicate `already_reacted` responses as idempotent success so repeated agent reaction adds no longer surface as tool failures. Fixes #69005. Thanks @shipitsteven and @martingarramon.
|
||||
- Channels/Discord: cool down Cloudflare/Error 1015 HTML 429 REST failures during startup application lookup and gateway metadata fetches, add `channels.discord.applicationId` as an app-id lookup bypass, sanitize HTML bodies before logging, and honor Retry-After before falling back to a conservative cooldown. Fixes #38853. (#74489) Thanks @djgeorg3 and @Garyko0730.
|
||||
- Slack/tools: expose `fileId` in the shared message tool schema so `download-file` can receive Slack attachment IDs from inbound placeholders. Fixes #45574. Thanks @chadvegas.
|
||||
- Exec: reject invalid per-call `host` values instead of silently falling back to the default target, so hostname-like values fail before commands run. Fixes #74426. Thanks @scr00ge-00 and @vyctorbrzezowski.
|
||||
- Google/Gemini: send non-empty placeholder content when a Gemini run is triggered with empty or filtered user content, avoiding `contents is not specified` API errors. Thanks @CaoYuhaoCarl.
|
||||
@@ -133,8 +62,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Cron/Gateway: abort and bounded-clean up timed-out isolated agent turns before recording the timeout, so stale cron sessions cannot leave Discord or other chat lanes stuck in `processing` after a timeout. Thanks @vincentkoc.
|
||||
- Agents/errors: suppress malformed streaming tool-call JSON fragments before they reach chat surfaces while preserving provider request-validation diagnostics. Fixes #59076; keeps #59080 as duplicate coverage. (#59118) Thanks @singleGanghood.
|
||||
- CLI/models: restore provider-filtered `models list --all --provider <id>` rows for providers without manifest/static catalog coverage, including Anthropic and Amazon Bedrock, while keeping the compatibility fallback off expensive availability and resolver paths. Thanks @shakkernerd.
|
||||
- CLI/models: keep manifest auth-evidence credentials visible across `models status`, auth probes, and PI model discovery so workspace-scoped provider auth does not disagree between listing, probing, and execution. Thanks @shakkernerd.
|
||||
- CLI/models: move local credential evidence such as Google Vertex ADC into generic plugin manifest setup metadata so the model-list auth index stays declarative without provider-specific runtime branches. Thanks @shakkernerd.
|
||||
- CLI/models: compute the `models list` Auth column through one command-local provider auth index so row rendering no longer repeats auth profile, env, configured-provider, AWS, or synthetic-auth checks per model row. Thanks @shakkernerd.
|
||||
- CLI/models: move the OpenAI listable catalog into the plugin manifest so `models list --all --provider openai` uses the manifest fast path instead of loading provider runtime normalization hooks. Thanks @shakkernerd.
|
||||
- CLI/tools: keep the Gateway `tools.*` RPC namespace out of plugin command discovery and managed proxy startup, so stray commands like `openclaw tools effective` fail quickly instead of cold-loading plugin metadata. Refs #73477. Thanks @oromeis.
|
||||
@@ -154,7 +81,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels/Telegram: honor `ALL_PROXY` / `all_proxy` and service-level `OPENCLAW_PROXY_URL` when constructing the HTTP/1-only Telegram Bot API transport, so Windows and service installs that rely on those proxy settings no longer fall back to direct egress. Fixes #74014; refs #74086. Thanks @SymbolStar.
|
||||
- Channels/Telegram: keep raw host/network-unreachable Bot API connect failures non-fatal and route tagged polling uncaught exceptions through the Telegram restart path, so transient reachability failures no longer kill the Gateway or leave long polling stuck. Fixes #60515; refs #74540. Thanks @HemantSudarshan, @thacid22, and @ewimsatt.
|
||||
- Channels/Telegram: continue polling when `deleteWebhook` hits a transient network failure but `getWebhookInfo` confirms no webhook is configured, so startup does not retry cleanup forever after the webhook was already removed. Refs #74086; carries forward #47384. Thanks @clovericbot.
|
||||
- Channels/Telegram: retry native quote replies without `reply_parameters.quote` when Telegram returns `QUOTE_TEXT_INVALID`, so stale or truncated quote excerpts no longer drop the whole reply. Fixes #74581. Thanks @moeedahmed.
|
||||
- Channels/Telegram: apply strict safe-send retry to inbound final replies when grammY wraps a pre-connect failure, while leaving ambiguous plain network envelopes single-shot to avoid duplicate visible messages. Fixes #74203. Thanks @nanli2000cn.
|
||||
- Channels/Telegram: surface polling liveness warnings in channel status and doctor when a running long-poller has not completed `getUpdates` after startup grace or its transport activity is stale, so silent polling failures no longer look clean. Refs #74299. Thanks @lolaopenclaw.
|
||||
- Channels/Telegram: publish webhook runtime state and warn when `setWebhook` has not completed after startup grace, so webhook-mode accounts no longer look healthy while registration is still failing or retrying. Refs #74299. Thanks @lolaopenclaw and @martingarramon.
|
||||
@@ -248,7 +174,6 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI/status: fall back to a bounded local `status` RPC when loopback detail probes time out or report unknown capability, so reachable local gateways are no longer marked unreachable by slow read diagnostics. Fixes #73535; refs #48360, #62762, #51357, and #42019. Thanks @RacecarGuy, @justinschille, @DJBlackhawk, @tianyaqpzm, and @0xrsydn.
|
||||
- CLI/gateway: reuse cached paired-device auth during `gateway probe` and report post-connect diagnostic failures as degraded reachability, so healthy local gateways are no longer marked unreachable after loopback auth or read timeouts. Fixes #48360. Thanks @RacecarGuy.
|
||||
- Channels/Discord: give Discord Gateway WebSocket handshakes a 30s timeout so stalled TLS/network transitions emit an error and Carbon can continue its reconnect loop instead of leaving the bot silent until restart. Refs #50046. Thanks @codexGW.
|
||||
- Mattermost/WebSocket: send protocol ping/pong keepalives and terminate stale sessions when pongs stop arriving, so silent TCP drops reconnect instead of leaving monitoring idle. Fixes #41837; carries forward #57621; refs #50138, #44160, and #51104. Thanks @JasonWang1124.
|
||||
- Channels/Telegram: suppress standalone failed edit/write warning payloads when a user-facing assistant error reply already covers the turn, while keeping unresolved mutating failures visible behind success-looking or suppressed-error replies. Fixes #39631; refs #73750; carries forward #39636 and #39717; leaves #39406 for configurable delivery policy. Thanks @Bartok9 and @Bortlesboat.
|
||||
- Control UI/agents: persist the Set Default action through `agents.list[].default` instead of writing the unsupported `agents.defaultId` field, so saved default-agent changes survive config validation. Fixes #65565; carries forward #72585. Thanks @luyao618.
|
||||
- NVIDIA/NIM: persist the `NVIDIA_API_KEY` provider marker and mark bundled NVIDIA Chat Completions models as string-content compatible, so NIM models load from `models.json` and OpenAI-compatible subagent calls send plain text content. Fixes #73013 and #50107; refs #73014. Thanks @bautrey, @iot2edge, @ifearghal, and @futhgar.
|
||||
@@ -296,7 +221,6 @@ Docs: https://docs.openclaw.ai
|
||||
- Pairing/doctor: bootstrap `commands.ownerAllowFrom` from the first approved DM pairing when no command owner exists, and have doctor explain missing owners so privileged slash commands are not accidentally unusable after onboarding. Thanks @pashpashpash.
|
||||
- Telegram/exec: infer native exec approvers from `commands.ownerAllowFrom` and auto-enable the Telegram approval client when an owner is resolvable, so owner-only commands such as `/diagnostics` can be approved in Telegram without duplicate per-channel approver config. Thanks @pashpashpash.
|
||||
- Auto-reply/session: carry the tail of user/assistant turns into the freshly-rotated transcript on silent in-reply session resets (compaction failure, role-ordering conflict) so direct-chat continuity survives the rebind. Fixes #70853. (#70898) Thanks @neeravmakwana.
|
||||
- Skills: load grouped skill directories such as `skills/<group>/<skill>/SKILL.md` from configured skill roots while keeping grouped discovery capped for large directories. Fixes #56915. (#72534) Thanks @ottodeng, @MoerAI, and @i010542.
|
||||
- Config: skip malformed non-string `env.vars` entries before env-reference checks, so config loading no longer crashes on JSON values like numbers or booleans. (#42402) Thanks @MiltonHeYan.
|
||||
- Docker Compose: default missing config and workspace bind mounts to `${HOME:-/tmp}/.openclaw` so manual compose runs do not create invalid empty-source volume specs. (#64485) Thanks @jlapenna.
|
||||
- Agents/context engines: preserve the child agent's configured `agentDir` when subagent cleanup re-resolves a context engine, so `onSubagentEnded` hooks keep operating on the correct per-agent state. (#67243) Thanks @jarimustonen.
|
||||
@@ -305,7 +229,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels/WhatsApp: detect explicit group `@mentions` again when the bot's own E.164 is in `allowFrom`, so shared-number setups no longer skip group pings that directly mention the bot. Fixes #49317. (#73453) Thanks @juan-flores077.
|
||||
- WhatsApp/reliability: publish real transport-liveness into WhatsApp channel status and force earlier reconnects on silent transport stalls, so quiet healthy sessions stay connected while wedged sockets recover before the later remote 408 path. (#72656) Thanks @Sathvik-1007.
|
||||
- Core/channels: tighten selected runtime, media, and plugin edge-case handling while preserving existing behavior. Thanks @jesse-merhi.
|
||||
- Channels/WhatsApp: strip leaked plural tool-call XML wrappers on every WhatsApp-visible outbound path and keep channel error payloads out of WhatsApp chats. (#71830) Thanks @rubencu.
|
||||
- Channels/WhatsApp: strip leaked plural tool-call XML wrappers on every WhatsApp-visible outbound path and allow `channels.whatsapp.exposeErrorText` to suppress visible error text per channel or account. (#71830) Thanks @rubencu.
|
||||
- Agents/embedded-runner: inject the resolved OAuth bearer (and forward the run abort signal) on the boundary-aware embedded stream fallback so models that route through `openai-codex-responses` and other boundary-aware transports stop failing with `401 Unauthorized: Missing bearer or basic authentication in header`. Fixes #73559. (#73588) Thanks @openperf.
|
||||
- Telegram/gateway: bound outbound Bot API calls and cache bundled plugin alias lookup so slow Telegram sends or WSL2 filesystem scans no longer wedge gateway replies. (#74210) Thanks @obviyus.
|
||||
- Configure/GitHub Copilot: reuse existing Copilot auth during configure and show the provider's manifest model catalog in the model picker. (#74276) Thanks @obviyus.
|
||||
@@ -315,20 +239,9 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/tool policy: validate caller group IDs against session or spawned context before applying group-scoped tool policies or persisting gateway group metadata, so forged group IDs cannot unlock more permissive tools. (#73720) Thanks @mmaps.
|
||||
- Commands: keep channel-prefixed owner allowlist entries scoped to matching providers so webchat command contexts cannot inherit external channel owners. Thanks @zsxsoft.
|
||||
- Auth/device pairing: bound bootstrap handoff token issuance, redemption, and approved pairing baselines to the documented per-role scope allowlist, so bootstrap approvals cannot persistently grant `operator.admin`, `operator.pairing`, or `node.exec` scopes. Thanks @eleqtrizit.
|
||||
- Providers/GitHub Copilot: support the GUI/RPC wizard device-code auth flow so onboarding from non-TTY clients (gateway RPC bridge, GUI wizards) completes instead of returning empty profiles. Dangerous-state handling now distinguishes `access_denied` and `expired_token` from transport errors. (#73290) Thanks @indierawk2k2.
|
||||
- Installer/Linux: warn before switching an unwritable npm global prefix to `~/.npm-global`, then tell users to run future global updates with `npm i -g openclaw@latest` without `sudo` so npm keeps using the redirected user prefix. Fixes #44365; carries forward #50479. Thanks @Sayeem3051.
|
||||
- Gateway/plugins: enable the native `require()` fast path on Windows for bundled plugin modules so plugin loading uses `require()` instead of Jiti's transform pipeline, reducing startup from ~39s to ~2s on typical 6-plugin setups. Fixes #68656. (#74173) Thanks @galiniliev.
|
||||
|
||||
## 2026.4.27
|
||||
|
||||
### Highlights
|
||||
|
||||
- Codex Computer Use setup now ships with status/install commands, marketplace discovery, and fail-closed MCP checks for Codex-mode desktop control. Thanks @pash-openai.
|
||||
- DeepInfra joins the bundled provider set with model discovery, media generation/editing, TTS, embeddings, and provider-owned onboarding policy. Thanks @ats3v.
|
||||
- Tencent Yuanbao and QQBot support expand channel coverage with Yuanbao docs/catalog entries and QQBot group chat, streaming, media upload, and pipeline refactors. Thanks @loongfay and @cxyhhhhh.
|
||||
- Plugin startup and model catalogs move toward manifest-first metadata, reducing Gateway boot work and making provider rows/aliases/suppressions easier to audit. Thanks @shakkernerd.
|
||||
- Reliability fixes cover Telegram startup/sends, Slack socket/media stalls, gateway startup prewarm, session/history defaults, update sync, and Windows restart handoffs. Thanks @joerod26, @obviyus, @shivasymbl, @freerk, @bassboy2k, @jpreagan, @islandpreneur007, and @Thatgfsj.
|
||||
|
||||
### Changes
|
||||
|
||||
- Sandbox/Docker: add opt-in `sandbox.docker.gpus` passthrough for Docker sandbox containers so local GPU workloads can run inside sandboxed agents when the host Docker runtime supports `--gpus`. Fixes #57976; carries forward #58124. Thanks @cyan-ember.
|
||||
@@ -2321,6 +2234,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory/active-memory: default QMD recall to search and surface better search-path telemetry so memory-backed recall works more predictably out of the box. (#65068) Thanks @Takhoffman.
|
||||
- Docs/providers: expand bundled provider docs with richer capability, env-var, and setup guidance across provider pages.
|
||||
- Docs/memory-wiki: add the recommended QMD + bridge-mode hybrid recipe plus zero-artifact troubleshooting guidance for `memory-wiki` bridge setups. (#63165) Thanks @sercada and @vincentkoc.
|
||||
- Agents/commitments: add opt-in inferred follow-up commitments with hidden batched extraction, per-agent/per-channel scoping, heartbeat delivery, CLI management, a simple `commitments.enabled`/`commitments.maxPerDay` config, and heartbeat-interval due-time clamping so magical check-ins do not echo immediately. (#74189) Thanks @vignesh07.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252
|
||||
FROM debian:bookworm-slim@sha256:4724b8cc51e33e398f0e2e15e18d5ec2851ff0c2280647e1310bc1642182655d
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
FROM debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252
|
||||
FROM debian:bookworm-slim@sha256:4724b8cc51e33e398f0e2e15e18d5ec2851ff0c2280647e1310bc1642182655d
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "646c710cf04fdf9e6c6ca935f3184924db3397a816848a7f8a8a3c10a4d8e9c8",
|
||||
"originHash" : "e6910acc97de62dc423c0a391985c1c2f28207951e356081539abde41f9ffc72",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "commander",
|
||||
@@ -15,8 +15,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftlang/swift-syntax.git",
|
||||
"state" : {
|
||||
"revision" : "9de99a78f099e59caf2b2beec65a4c45d54b2081",
|
||||
"version" : "603.0.1"
|
||||
"revision" : "0687f71944021d616d34d922343dcef086855920",
|
||||
"version" : "600.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -24,8 +24,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-testing",
|
||||
"state" : {
|
||||
"revision" : "937120cbc281cf29727fdfb8734482158508b4fc",
|
||||
"version" : "6.3.1"
|
||||
"revision" : "399f76dcd91e4c688ca2301fa24a8cc6d9927211",
|
||||
"version" : "0.99.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@ let package = Package(
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/steipete/Commander.git", exact: "0.2.2"),
|
||||
.package(url: "https://github.com/apple/swift-testing", from: "6.3.1"),
|
||||
.package(url: "https://github.com/apple/swift-testing", from: "0.99.0"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
340
appcast.xml
340
appcast.xml
@@ -2,298 +2,6 @@
|
||||
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
|
||||
<channel>
|
||||
<title>OpenClaw</title>
|
||||
<item>
|
||||
<title>2026.4.27</title>
|
||||
<pubDate>Wed, 29 Apr 2026 23:53:26 +0000</pubDate>
|
||||
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
|
||||
<sparkle:version>2026042790</sparkle:version>
|
||||
<sparkle:shortVersionString>2026.4.27</sparkle:shortVersionString>
|
||||
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
|
||||
<description><![CDATA[<h2>OpenClaw 2026.4.27</h2>
|
||||
<h3>Changes</h3>
|
||||
<ul>
|
||||
<li>Sandbox/Docker: add opt-in <code>sandbox.docker.gpus</code> passthrough for Docker sandbox containers so local GPU workloads can run inside sandboxed agents when the host Docker runtime supports <code>--gpus</code>. Fixes #57976; carries forward #58124. Thanks @cyan-ember.</li>
|
||||
<li>iOS/Gateway: add an authenticated <code>node.presence.alive</code> protocol event and <code>node.list</code> last-seen fields so background iOS wakes can mark paired nodes recently alive without treating them as connected. Carries forward #63123. Thanks @ngutman.</li>
|
||||
<li>Android: publish authenticated <code>node.presence.alive</code> events after node connect and background transitions so paired Android nodes retain durable last-seen metadata after disconnects. Carries forward #63123. Thanks @ngutman.</li>
|
||||
<li>Gateway/chat: accept non-image attachments through <code>chat.send</code> by staging them as agent-readable media paths, while keeping unsupported RPC attachment paths explicit instead of silently dropping files. Fixes #48123. (#67572) Thanks @samzong.</li>
|
||||
<li>Security/networking: add opt-in operator-managed outbound proxy routing (proxy.enabled + proxy.proxyUrl/OPENCLAW_PROXY_URL) with strict http:// forward-proxy validation, loopback-only Gateway bypass, and cleanup of proxy env/dispatcher state on exit. (#70044) Thanks @jesse-merhi and @joshavant.</li>
|
||||
<li>Dependencies: refresh provider and tooling dependencies, including AWS SDK, PI runtime packages, AJV, Feishu SDK, Anthropic SDK, tokenjuice, and native TypeScript/oxlint tooling. Thanks @dependabot.</li>
|
||||
<li>Matrix/QA: add live Matrix approval scenarios for exec metadata, chunked fallback, plugin approvals, deny reactions, thread targeting, and <code>target: "both"</code> delivery, with redacted artifacts preserving safe approval summaries. Thanks @gumadeiras.</li>
|
||||
<li>Codex: add Computer Use setup for Codex-mode agents, including <code>/codex computer-use status/install</code>, marketplace discovery, optional auto-install, and fail-closed MCP server checks before Codex-mode turns start. Fixes #72094. (#71842) Thanks @pash-openai.</li>
|
||||
<li>Apps: consume Peekaboo 3.0.0-beta4 and ElevenLabsKit 0.1.1, align Swabble on Commander 0.2.2, and refresh macOS/iOS SwiftPM resolutions against the released dependency graph. Thanks @Blaizzy.</li>
|
||||
<li>Plugin SDK: expose shared channel route normalization, parser-driven target resolution, raw-target compact keys, parsed-target types, and route comparison helpers through <code>openclaw/plugin-sdk/channel-route</code>, switch native approval origin matching onto that route contract with optional delivery and match-only target normalization, and retire the internal channel-route shim behind dated compatibility aliases for legacy key/comparable-target helpers. Thanks @vincentkoc.</li>
|
||||
<li>Docs/Codex: document how Codex Computer Use, direct <code>cua-driver mcp</code>, and OpenClaw.app's PeekabooBridge fit together so desktop-control setup choices are clearer. Thanks @pash-openai and @trycua.</li>
|
||||
<li>Matrix/streaming: stream tool-progress updates into live Matrix preview edits by default when preview streaming is active, with <code>streaming.preview.toolProgress: false</code> to keep answer previews while hiding interim tool lines. Thanks @gumadeiras.</li>
|
||||
<li>Plugins/models: wire manifest <code>modelCatalog.aliases</code> and <code>modelCatalog.suppressions</code> into model-catalog planning and built-in model suppression, with stale Spark and Qwen Coding Plan suppressions now declared in plugin manifests instead of runtime fallback hooks. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/models: add a shared manifest-backed provider catalog builder and move Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Moonshot, DeepSeek, Tencent TokenHub, and StepFun provider catalogs onto their plugin manifest <code>modelCatalog</code> rows. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/models: move BytePlus and Volcano Engine standard and plan-provider catalogs into plugin manifest <code>modelCatalog</code> rows and remove the now-unused Volcengine-family shared catalog SDK subpath. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: move Fireworks and Together AI fixed provider catalogs into plugin manifest <code>modelCatalog</code> rows so provider-filtered listing can use manifest-backed static rows. Thanks @shakkernerd.</li>
|
||||
<li>Channels/Yuanbao: register the Tencent Yuanbao external channel plugin (<code>openclaw-plugin-yuanbao</code>) in the official channel catalog, contract suites, and community plugin docs, with a new <code>docs/channels/yuanbao.md</code> quick-start guide for WebSocket bot DMs and group chats. (#72756) Thanks @loongfay.</li>
|
||||
<li>Channels/Yuanbao: add a channel docs entrance so the Tencent Yuanbao bot appears in the channel listing and sidebar navigation. (#73443) Thanks @loongfay.</li>
|
||||
<li>Channels/QQBot: add full group chat support (history tracking, @-mention gating, activation modes, per-group config, FIFO message queue with deliver debounce), C2C <code>stream_messages</code> streaming with a <code>StreamingController</code> lifecycle manager, unified <code>sendMedia</code> with chunked upload for large files, and refactor the engine into pipeline stages, focused outbound submodules, builtin slash-command modules, and explicit DI ports via <code>createEngineAdapters()</code>. (#70624) Thanks @cxyhhhhh.</li>
|
||||
<li>Plugins/startup: migrate bundled plugin manifests to explicit <code>activation.onStartup</code> declarations so Gateway startup imports only the bundled plugins that intentionally register startup-time runtime surfaces. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add an opt-in future-mode gate for disabling deprecated implicit startup sidecar loading while preserving explicit startup and narrower activation triggers. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add plugin compatibility warnings for deprecated implicit startup loading so authors can migrate to explicit <code>activation.onStartup</code> metadata. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/runtime: load bundled agent tool-result middleware from manifest contracts on demand so tokenjuice stays startup-lazy without losing Pi/Codex tool-output compaction. Thanks @shakkernerd.</li>
|
||||
<li>Plugins/startup: add explicit <code>activation.onStartup</code> metadata so plugins can declare Gateway startup import behavior while the deprecated implicit sidecar fallback remains for legacy plugins. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/startup: reuse lookup-table plugin manifests when loading startup plugins so Gateway boot avoids rebuilding plugin discovery and manifest metadata. Thanks @shakkernerd.</li>
|
||||
<li>CLI/models: declare fixed Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Chutes, Kilo, OpenAI, and OpenCode Go model catalogs in refreshable plugin manifests, keep broad <code>models list --all</code> on raw registry and supplement rows without runtime normalization, and avoid duplicate supplement resolution. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/runtime: reuse the current plugin metadata snapshot for provider discovery so repeated model-provider discovery avoids rebuilding plugin manifest metadata. Thanks @shakkernerd.</li>
|
||||
<li>Gateway/startup: pass the plugin metadata snapshot from config validation into plugin bootstrap so startup reuses one manifest product instead of rebuilding plugin metadata. Thanks @shakkernerd.</li>
|
||||
<li>Plugin SDK/testing: move core-only channel contract fixtures under the channel contract test tree and retire the old <code>test/helpers/channels</code> bridge directory so plugin tests stay on focused SDK surfaces. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose native agent-runtime contract fixtures through <code>plugin-sdk/agent-runtime-test-contracts</code>, move sandbox config fixtures into the focused generic fixture subpath, and block extension tests from importing repo-only <code>test/helpers</code> bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose generic module reload, bundled-path, Node builtin mock, channel pairing/envelope, HTTP server, temp-home, replay-policy, and live STT helpers through focused SDK test subpaths so extension tests no longer depend on repo-only helper bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK: move maintained bundled channels off the deprecated <code>channel-config-schema-legacy</code> subpath, add an explicit bundled-channel schema SDK surface, and track both remaining legacy test/config compatibility barrels with dated removal windows. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose media provider capability assertions and provider HTTP mocks through focused SDK test subpaths, and retire the repo-only media-generation test helper bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: promote bundled plugin/provider/channel contract helpers to focused SDK test subpaths and retire the repo-only <code>test/helpers/plugins</code> TypeScript bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: expose generic channel action, setup, status, and directory contract helpers through <code>plugin-sdk/channel-test-helpers</code> so bundled extension tests no longer import repo-only channel helper bridges. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add <code>plugin-sdk/channel-target-testing</code> for shared channel target-resolution cases, document channel reaction helpers on <code>plugin-sdk/channel-feedback</code>, and keep the old <code>plugin-sdk/test-utils</code> alias as compatibility-only. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add a focused generic fixture subpath for CLI capture, sandbox, skill, agent-message, system-event, terminal, chunking, auth-token, and typed-case helpers. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add focused plugin runtime and environment fixture subpaths so plugin tests can avoid the broad <code>plugin-sdk/testing</code> barrel for common setup helpers. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK/testing: add a focused <code>plugin-sdk/plugin-test-api</code> helper subpath and move bundled plugin registration tests off the repo-only plugin API bridge. Thanks @vincentkoc.</li>
|
||||
<li>Plugin SDK: add generic host hooks for session state, next-turn context, trusted tool policy, UI descriptors, events, scheduler cleanup, and run-scoped plugin context. (#72287) Thanks @100yenadmin.</li>
|
||||
<li>Plugin SDK/testing: expose provider catalog, wizard, registry, manifest, public-artifact, outbound, and TTS contract helpers through documented SDK testing seams so bundled plugin tests no longer import repo <code>src/**</code> internals. Thanks @vincentkoc.</li>
|
||||
<li>Providers/DeepInfra: add a bundled DeepInfra provider with <code>DEEPINFRA_API_KEY</code> onboarding, dynamic OpenAI-compatible model discovery, image generation/editing, image/audio media understanding, TTS, text-to-video, memory embeddings, static catalog metadata, and provider-owned base URL policy. Carries forward #53805, #48088, #37576, #43896, #11533, and #2554. Thanks @ats3v.</li>
|
||||
<li>Matrix: attach versioned structured approval metadata to pending approval messages so capable Matrix clients can render richer approval UI while body text and reaction fallback keep working. (#72432) Thanks @kakahu2015.</li>
|
||||
</ul>
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
<li>Gateway/sessions: align <code>chat.history</code> and <code>sessions.list</code> thinking defaults with owning-agent and catalog-aware resolution so Control UI session defaults match backend runtime state. (#63418) Thanks @jpreagan.</li>
|
||||
<li>Devices/pairing: recover array-shaped device and node pairing state files before persisting approvals, so UUID-keyed pending and paired entries no longer disappear after a malformed JSON store write. Fixes #63035. Thanks @sar618.</li>
|
||||
<li>Gateway/auth: clear reused stale device tokens and stop reconnecting on device-token mismatch in the Control UI and Node gateway clients, avoiding rate-limit loops after scope-upgrade or token-rotation handoffs. Fixes #71609. Thanks @ricksayhi.</li>
|
||||
<li>Gateway/approvals: treat duplicate same-decision approval resolves as idempotent during the resolved-entry grace window, including consumed <code>allow-once</code> approvals, while returning an explicit already-resolved error for conflicting repeats. Fixes #59162; refs #58479 and #65486. Thanks @wikithoughts, @sajazuniga7-coder, and @mjmai20682068-create.</li>
|
||||
<li>Channels/Telegram: honor <code>approvals.exec/plugin.targets[].accountId</code> when routing native approvals across multi-bot Telegram accounts while preserving unscoped Telegram targets for any account. Fixes #69916. Thanks @joerod26.</li>
|
||||
<li>Telegram/gateway: bound outbound Bot API calls and cache bundled plugin alias lookup so slow Telegram sends or WSL2 filesystem scans no longer wedge gateway replies. (#74210) Thanks @obviyus.</li>
|
||||
<li>Agents/exec: omit the internal session-resume fallback preface from successful async exec completion messages sent directly back to chat. Fixes #67181. Thanks @raistlin88.</li>
|
||||
<li>Agents/media: register detached <code>video_generate</code> and <code>music_generate</code> tool run contexts until terminal status, so Discord-backed provider jobs stay live in <code>/tasks</code> instead of becoming <code>lost</code> when the parent chat run context disappears. Thanks @vincentkoc.</li>
|
||||
<li>Agents/media: prefer OpenAI image and video providers when the default model uses the OpenAI Codex auth alias, so auto media generation no longer falls through to Fal before GPT Image or Sora. Thanks @vincentkoc.</li>
|
||||
<li>Tasks/media: infer agent ownership for session-scoped task records so <code>/tasks</code> agent-local fallback includes session-backed <code>video_generate</code> and other async media jobs even when the current chat session has no linked rows. Thanks @vincentkoc.</li>
|
||||
<li>Agents/media: keep long-running <code>video_generate</code> and <code>music_generate</code> tasks fresh while provider jobs are still pending, so task maintenance does not mark active Discord media renders lost before completion. Thanks @vincentkoc.</li>
|
||||
<li>CLI/status: treat scope-limited gateway probes as reachable-but-degraded in shared status scans, so <code>openclaw status --all</code> no longer reports a live gateway as unreachable after <code>missing scope: operator.read</code>. Fixes #49180; supersedes #47981. Thanks @openjay.</li>
|
||||
<li>CLI/update: skip tracked plugins disabled in config during post-update plugin sync before npm, ClawHub, or marketplace update checks, preserving their install records without failing the update. Fixes #73880. Thanks @islandpreneur007.</li>
|
||||
<li>Slack/Socket Mode: use a 15s Slack SDK pong timeout by default and add <code>channels.slack.socketMode.clientPingTimeout</code>, <code>serverPingTimeout</code>, and <code>pingPongLoggingEnabled</code> overrides so stale-websocket handling no longer depends on app-event health heuristics. Fixes #14248; refs #58519, #64009, and #63488. Thanks @shivasymbl and @freerk.</li>
|
||||
<li>Slack/media: bound private file and forwarded attachment downloads with idle and total timeouts while preserving placeholder fallback, so stalled Slack <code>file_share</code> media no longer wedges inbound message handling. Fixes #61850. Thanks @bassboy2k.</li>
|
||||
<li>Plugins/inspector: keep bundled plugin runtime capture quiet and config-tolerant for Codex, memory-lancedb, Feishu, Mattermost, QQBot, and Tlon so plugin-inspector JSON checks can validate the full bundled set. Thanks @vincentkoc.</li>
|
||||
<li>Slack/auto-reply: keep fully consumed text reset triggers such as <code>new session</code> out of <code>BodyForAgent</code> after directive cleanup, so configured Slack reset phrases do not leak into the fresh model turn. Fixes #73137. Thanks @neeravmakwana.</li>
|
||||
<li>Plugins/runtime deps: prune stale retained bundled runtime deps and keep doctor/secret channel contract scans on lightweight artifacts, so disabled bundled channels stop preserving old dependency trees or importing heavy plugin surfaces. Thanks @SymbolStar and @vincentkoc.</li>
|
||||
<li>Plugins/runtime deps: cache unchanged bundled runtime mirror dist-file materialization decisions and close file-lock handles on owner-write failures, reducing repeated startup chunk scans and avoiding FileHandle-GC recovery stalls. Refs #73532. Thanks @oadiazp and @bstanbury.</li>
|
||||
<li>Auto-reply: bound the post-run pending tool-result delivery drain with a progress-aware idle timeout, so a never-settling tool-result task no longer leaves the session active forever while slow healthy deliveries can keep draining. Fixes #53889; supersedes #64733 and #73434. Thanks @zijunl and @wujiaming88.</li>
|
||||
<li>Gateway/startup: start chat channels without waiting for primary model prewarm, keeping model warmup bounded in the background so Slack and other channels come online promptly when provider discovery is slow. Supersedes #73420. Thanks @dorukardahan.</li>
|
||||
<li>Gateway/install: carry env-backed config SecretRefs such as <code>channels.discord.token</code> into generated service environments when they are present only in the installing shell, while keeping gateway auth SecretRefs non-persisted. Fixes #67817; supersedes #73426. Thanks @wdimaculangan and @ztexydt-cqh.</li>
|
||||
<li>Auto-reply/commands: stop bare <code>/reset</code> and <code>/new</code> after reset hooks acknowledge the command, so non-ACP channels no longer fall through into empty provider calls while <code>/reset <message></code> and <code>/new <message></code> still seed the next model turn. Fixes #73367 and #73412. Thanks @hoyanhan, @wenxu007, and @amdhelper.</li>
|
||||
<li>Providers/DeepSeek: backfill DeepSeek V4 <code>reasoning_content</code> on plain assistant replay messages as well as tool-call turns, so thinking sessions with prior tool use no longer fail follow-up requests with missing reasoning content. Fixes #73417; refs #71372. Thanks @34262315716 and @Bartok9.</li>
|
||||
<li>Agents/gateway tool: strip full config payloads from <code>config.patch</code> and <code>config.apply</code> tool responses while preserving direct RPC responses, so config-heavy sessions no longer replay large redacted configs into transcript history. Fixes #47610; supersedes #73439. Thanks @HanenVit and @juan-flores077.</li>
|
||||
<li>Auto-reply: preserve voice-note media from silent turns while continuing to suppress text and non-voice media, so <code>NO_REPLY</code> TTS replies still deliver the requested audio bubble. (#73406) Thanks @zqchris.</li>
|
||||
<li>Channels/Mattermost: stop enqueueing regular inbound posts as system events, so Mattermost user messages reach the model only as user-role inbound-envelope content instead of also appearing as <code>System: Mattermost message...</code> directives. Fixes #71795. Thanks @juan-flores077.</li>
|
||||
<li>Agents/media: qualify bare <code>agents.defaults.imageModel</code> and <code>pdfModel</code> refs from unique configured image-capable providers, so Ollama vision models such as <code>moondream</code> and <code>qwen2.5vl:7b</code> do not fall through to the default provider. Fixes #38816; supersedes #73396. Thanks @alainasclaw and @vincentkoc.</li>
|
||||
<li>Agents/Anthropic: send implicit Anthropic beta headers only to direct public Anthropic endpoints, including OAuth, so custom Anthropic-compatible providers no longer mis-handle unsupported beta flags unless explicitly configured. Refs #73346. Thanks @byBrodowski.</li>
|
||||
<li>Skills: require explicit <code>skills.entries.coding-agent.enabled</code> before exposing the bundled coding-agent skill, so installs with Codex on PATH but no OpenAI auth do not silently offer Codex delegation. Fixes #73358. Thanks @LaFleurAdvertising and @Sanjays2402.</li>
|
||||
<li>Plugins/startup: treat manifestless Claude bundles as valid installed-plugin registry entries instead of stale missing manifests, so workspace bundles no longer force repeated derived registry rebuilds or noisy <code>plugins.entries.workspace</code> warnings during Gateway startup. Fixes #73433. Thanks @AnneVoss.</li>
|
||||
<li>Agents/subagents: preserve <code>sessions_yield</code> as a paused subagent state and ignore its wait text while freezing completion output, so parent sessions wait for the final post-compaction answer instead of receiving intermediate progress or <code>(no output)</code>. Fixes #73413. Thanks @Ask-sola.</li>
|
||||
<li>Plugins/startup: precompute bundled runtime mirror fingerprints before taking the mirror lock and keep Docker bundled plugin runtime deps/mirrors in a Docker-managed volume instead of the Windows/WSL config bind mount, so cold starts avoid slow host-volume mirror writes. Fixes #73339. Thanks @1yihui.</li>
|
||||
<li>Plugins/runtime deps: refresh bundled runtime mirrors without deleting active import trees, so config-triggered restarts do not see transient missing plugin files during registration. Thanks @shakkernerd.</li>
|
||||
<li>Channels/LINE: persist inbound image, video, audio, and file downloads in <code>~/.openclaw/media/inbound/</code> instead of temporary files so agents can still read LINE media after <code>/tmp</code> cleanup. Fixes #73370. Thanks @hijirii and @wenxu007.</li>
|
||||
<li>CLI/plugins: keep bundled plugin installs out of <code>plugins.load.paths</code> while preserving install records, so install/inspect/doctor loops no longer warn about the current bundled plugin directory. Thanks @vincentkoc.</li>
|
||||
<li>CLI/plugins: scope <code>plugins inspect <id></code> runtime loading to the matched plugin so single-plugin inspection does not load every plugin before checking the target. Thanks @shakkernerd.</li>
|
||||
<li>CLI/plugins: remove managed copied-path plugin directories during uninstall and plan uninstall from metadata instead of runtime-loading plugins, so plugin lifecycle commands avoid unnecessary bundled runtime-deps work. Thanks @shakkernerd.</li>
|
||||
<li>Cron tool: infer the creating session's agentId for <code>cron.add</code> jobs when <code>agentId</code> is omitted or passed as undefined, keeping scheduled agentTurn jobs routed to the session agent; #40571 identified the guard bug and supplied the focused regression coverage. Thanks @ChanningYul.</li>
|
||||
<li>Cron/Telegram: add <code>--thread-id</code> to <code>openclaw cron add</code> and <code>openclaw cron edit</code>, preserving Telegram forum topic delivery targets across scheduled announcements. Carries forward #51581, #60373, and #60890. Thanks @ChunHao-dev.</li>
|
||||
<li>Cron/Telegram: preserve session-derived Telegram topic thread IDs when isolated cron delivery explicitly targets the parent chat, keeping bare chat targets in the active forum topic without leaking stale topics to other chats. Carries forward #64708. Thanks @addelh.</li>
|
||||
<li>Memory/compaction: keep pre-compaction memory-flush prompts runtime-only so session transcripts and <code>chat.history</code> no longer expose them as normal user turns. Fixes #54408 and #58956; refs #43567. Thanks @markgong and @guoyuhang9.</li>
|
||||
<li>Control UI/WebChat: keep large attachment payloads out of Lit state and optimistic chat messages, using object URL previews plus send-time payload serialization so PDF/image uploads no longer trigger <code>RangeError: Maximum call stack size exceeded</code>. Fixes #73360; refs #54378 and #63432. Thanks @hejunhui-73, @Ansub, and @christianhernandez3-afk.</li>
|
||||
<li>Agents/Anthropic: cancel stalled Anthropic Messages SSE body reads when abort signals fire, so active-memory timeouts release transport resources instead of leaving hidden recall runs parked on <code>reader.read()</code>. Refs #72965 and #73120. Thanks @wdeveloper16.</li>
|
||||
<li>Control UI/WebChat: keep pending run and typing state attached to the active client run, so unowned inject/announce/side-result finals no longer unlock unrelated active runs while completed owned runs still clear promptly. Fixes #57795; carries forward the narrow diagnosis from #57887. Thanks @haoyu-haoyu.</li>
|
||||
<li>Sandbox/Docker: stop satisfying a missing default sandbox image by tagging plain Debian as <code>openclaw-sandbox:bookworm-slim</code>, preserving the Python tooling required by sandbox write/edit helpers and directing users to build the default image. Fixes #51185; refs #45108, #51099, #51609, and #57713. Thanks @dpalis, @Tin55FoilDev, @jbcohen2-coder, @macminihal-cyber, and @PraxoOnline.</li>
|
||||
<li>Control UI/WebChat: confirm toolbar New Session button resets before dispatching <code>/new</code> while leaving typed <code>/new</code> and <code>/reset</code> commands immediate. Fixes #45800; refs #27065, #56611, #54499, and #27110. Thanks @aethnova, @kosta228-huli, @adambezemek, and @xss925175263 (xianshishan).</li>
|
||||
<li>Agents/models: keep per-agent primary models strict when <code>fallbacks</code> is omitted, so probe-only custom providers are not tried as hidden fallback candidates unless the agent explicitly opts in. Fixes #73332. Thanks @haumanto.</li>
|
||||
<li>Gateway/models: add <code>models.pricing.enabled</code> so offline or restricted-network installs can skip startup OpenRouter and LiteLLM pricing-catalog fetches while keeping explicit model costs working. Fixes #53639. Thanks @callebtc, @palewire, and @rjdjohnston.</li>
|
||||
<li>Gateway/startup: warn when legacy <code>CLAWDBOT_*</code> or <code>MOLTBOT_*</code> environment variables are still present, pointing users to <code>OPENCLAW_*</code> names instead of failing silently. Fixes #53482; carries forward #53667. Thanks @lndyzwdxhs.</li>
|
||||
<li>Onboarding: pin interactive and non-interactive health checks to the just-configured setup token/password so stale <code>OPENCLAW_GATEWAY_TOKEN</code> or <code>OPENCLAW_GATEWAY_PASSWORD</code> values do not produce false gateway-token-mismatch failures after setup. Fixes #72203. Thanks @galiniliev.</li>
|
||||
<li>Doctor/state: require an interactive confirmation before archiving orphan transcript files, so <code>openclaw doctor --fix</code> no longer silently renames recoverable session history after upgrades regenerate <code>sessions.json</code>. Fixes #73106. Thanks @scottgl9.</li>
|
||||
<li>Cron/Telegram: preserve explicit <code>:topic:</code> delivery targets over stale session-derived thread IDs when isolated cron announces to Telegram forum topics. Carries forward #59069; refs #49704 and #43808. Thanks @roytong9.</li>
|
||||
<li>Build/runtime: write the runtime-postbuild stamp after <code>pnpm build</code> writes the build stamp, so the next CLI invocation does not re-sync runtime artifacts after a successful build. Fixes #73151. Thanks @bittoby.</li>
|
||||
<li>Build/runtime: preserve staged bundled-plugin runtime dependency caches across source-checkout tsdown rebuilds, so local CLI and gateway-watch rebuilds no longer recreate large plugin dependency trees before starting. Refs #73205. Thanks @SymbolStar.</li>
|
||||
<li>CLI/channels: list configured chat channel accounts from read-only setup metadata even when the standalone CLI has not loaded the runtime channel registry, so <code>openclaw channels list</code> shows Telegram accounts before auth providers. Fixes #73319 and #73322. Thanks @mlaihk.</li>
|
||||
<li>CLI/model probes: keep <code>infer model run --gateway</code> raw by skipping prior session transcript, bootstrap context, context-engine assembly, tools, and bundled MCP servers, so local backends can be tested without full agent-context overhead. Fixes #73308. Thanks @ScientificProgrammer.</li>
|
||||
<li>CLI/image describe: pass <code>--prompt</code> and <code>--timeout-ms</code> through <code>infer image describe</code> and <code>describe-many</code>, so custom vision instructions and slow local model budgets reach media-understanding providers such as Ollama, OpenAI, Google, and OpenRouter. Addresses #63700. Thanks @cedricjanssens.</li>
|
||||
<li>Providers/Ollama: reject long non-linguistic Kimi/GLM symbol runs as provider failures instead of storing them as successful visible assistant replies, so fallback or error handling can recover from garbled cloud output. Fixes #64262; refs #67019. Thanks @Kloz813 and @xiaomenger123.</li>
|
||||
<li>CLI/model probes: reject empty or whitespace-only <code>infer model run --prompt</code> values before calling local providers or the Gateway, so smoke checks do not spend provider calls on invalid turns. Fixes #73185. Thanks @iot2edge.</li>
|
||||
<li>Gateway/media: route text-only <code>chat.send</code> image offloads through media-understanding fields so <code>agents.defaults.imageModel</code> can describe WebChat attachments instead of leaving only an opaque <code>media://inbound</code> marker. Fixes #72968. Thanks @vorajeeah.</li>
|
||||
<li>Gateway/Windows: route no-listener restart handoffs through the Windows supervisor without leaving restart tokens in flight, so failed task scheduling can be retried and successful handoffs do not coalesce later restart requests. (#69056) Thanks @Thatgfsj.</li>
|
||||
<li>Gateway/model pricing: skip plugin manifest discovery during background pricing refreshes when <code>plugins.enabled: false</code>, so disabled-plugin setups do not keep rebuilding plugin metadata from the Gateway hot path. Fixes #73291. Thanks @slideshow-dingo and @fishgills.</li>
|
||||
<li>Ollama/thinking: validate <code>/think</code> commands against live Ollama catalog reasoning metadata and preserve explicit native <code>params.think</code>/<code>params.thinking</code>, so models whose <code>/api/show</code> capabilities include <code>thinking</code> expose <code>low</code>, <code>medium</code>, <code>high</code>, and <code>max</code> instead of being stuck on <code>off</code>. Fixes #73366. Thanks @cymise.</li>
|
||||
<li>Gateway/sessions: remove automatic oversized <code>sessions.json</code> rotation backups, deprecate <code>session.maintenance.rotateBytes</code>, and teach <code>openclaw doctor --fix</code> to remove the ignored key so hot session writes no longer copy multi-MB stores. Refs #72338. Thanks @midhunmonachan and @DougButdorf.</li>
|
||||
<li>Channels/Telegram: fail fast when Telegram rejects the startup <code>getMe</code> token probe with 401, so invalid or stale BotFather tokens are reported as token auth failures instead of misleading <code>deleteWebhook</code> cleanup failures. Fixes #47674. Thanks @samaedan-arch.</li>
|
||||
<li>ACPX: keep generated Codex and Claude ACP wrapper startup paths working when remote or special state filesystems reject chmod, since OpenClaw invokes the wrappers through Node instead of executing them directly. Fixes #73333. Thanks @david-garcia-garcia.</li>
|
||||
<li>CLI/onboarding: infer image input for common custom-provider vision model IDs, ask only for unknown models, and keep <code>--custom-image-input</code>/<code>--custom-text-input</code> overrides so vision-capable proxies do not get saved as text-only configs. Fixes #51869. Thanks @Antsoldier1974.</li>
|
||||
<li>Models/OpenAI Codex: stop listing or resolving unsupported <code>openai-codex/gpt-5.4-mini</code> rows through Codex OAuth, keep stale discovery rows suppressed with a clear API-key-route hint, and leave direct <code>openai/gpt-5.4-mini</code> available. Fixes #73242. Thanks @0xCyda.</li>
|
||||
<li>Plugin SDK: restore the root <code>stringEnum</code> and <code>optionalStringEnum</code> exports on both the published SDK entry and runtime root-alias bridge, so older external plugins can keep building and loading while migrating to focused SDK subpaths. Fixes #68279. Thanks @marzliak.</li>
|
||||
<li>Plugin SDK: restore the root-alias bridge for <code>registerContextEngine</code> and expose missing legacy compat helpers <code>normalizeAccountId</code> and <code>resolvePreferredOpenClawTmpDir</code> so older external plugins such as <code>openclaw-weixin</code> can keep loading while migrating to focused SDK subpaths. Fixes #53497. Thanks @alanxchen85.</li>
|
||||
<li>Auth profiles: make <code>openclaw doctor --fix</code> migrate legacy flat <code>auth-profiles.json</code> files such as <code>{ "ollama-windows": { "apiKey": "ollama-local" } }</code> to canonical provider default API-key profiles with a backup, so custom Ollama/OpenAI-compatible providers recover cleanly after upgrading. Fixes #59629; supersedes #59642. Thanks @Xsanders555 and @Linux2010.</li>
|
||||
<li>Memory/Dreaming: retry Dream Diary once with the session default when a configured dreaming model is unavailable, while leaving subagent trust and allowlist errors visible instead of silently masking configuration problems. Refs #67409 and #69209. Thanks @Ghiggins18 and @everySympathy.</li>
|
||||
<li>Feishu/inbound files: recover CJK filenames from plain <code>Content-Disposition: filename=</code> download headers when Feishu exposes UTF-8 bytes through Latin-1 header decoding, while leaving valid Latin-1 and JSON-derived names unchanged. (#48578, #50435, #59431) Thanks @alex-xuweilong, @lishuaigit, and @DoChaoing.</li>
|
||||
<li>Channels/Telegram: normalize accidental full <code>/bot<TOKEN></code> Telegram <code>apiRoot</code> values at runtime and teach <code>openclaw doctor --fix</code> to remove the suffix, so startup control calls no longer 404 when direct Bot API curl commands work. Fixes #55387. Thanks @brendanmatthewjones-cmyk, @techfindubai-ux, and @Sivlerback-Chris.</li>
|
||||
<li>Zalo Personal: persist refreshed <code>zca-js</code> session cookies after QR login, session restore, and successful API calls so gateway restarts restore the freshest local session. (#73277) Thanks @darkamenosa.</li>
|
||||
<li>Logging/security: redact sensitive tokens (sk-\* keys, Bearer/Authorization values, etc.) at the subsystem console sink so <code>createSubsystemLogger().info/warn/error</code> output that bypasses the patched console-capture handler still applies the same redaction the file transport already does. Fixes #73284; refs #67953 and #64046. Thanks @edwin-rivera-dev.</li>
|
||||
<li>Plugins/runtime deps: reuse enclosing versioned cache roots when bundled plugins resolve from nested staged paths, so plugin-runtime-deps no longer mints <code>openclaw-unknown-*</code> directories or loops on <code>ENOTEMPTY</code>. Fixes #72956. (#73205) Thanks @SymbolStar.</li>
|
||||
<li>Agents/failover: classify CJK provider transport, quota, billing, auth, and overload error text so Chinese-language provider failures trigger fallback and user-facing transport copy instead of surfacing as unclassified raw errors. (#56242) Thanks @tomcatzh.</li>
|
||||
<li>Agents/failover: seed non-claude-cli fallback prompts with Claude Code session context when a claude-cli attempt fails, so fallback models do not restart cold after billing or quota failover. (#72069) Thanks @stainlu.</li>
|
||||
<li>Agents/CLI runner: transfer bundle-MCP tempDir cleanup from the per-turn runner finally to the Claude live-session lifecycle, so persistent Claude CLI sessions keep their <code>--mcp-config</code> directory until the live subprocess closes. Fixes #73244. Thanks @edwin-rivera-dev.</li>
|
||||
<li>Gateway/nodes: allow Windows companion nodes to use safe declared commands such as canvas, camera list, location, device info, and screen snapshot by default while keeping dangerous media commands opt-in. (#71884) Thanks @shanselman.</li>
|
||||
<li>Agents/cron: clarify agent-tool and CLI cron timezone guidance so supplied <code>tz</code> values use local wall-clock cron fields and omitted cron <code>tz</code> falls back to the Gateway host local timezone. Fixes #53669; carries forward #46177. (#73372) Thanks @chen-zhang-cs-code and @maranello-o.</li>
|
||||
<li>Providers/Qwen: allow explicitly configured <code>qwen/qwen3.6-plus</code> to resolve on Qwen Coding Plan endpoints while keeping the built-in catalog from advertising it there. Fixes #63654; carries forward #63987. Thanks @jepson-liu.</li>
|
||||
<li>Channels/Telegram: keep Bot API network fallbacks sticky after failed attempts and retry timed-out startup control calls once on the fallback route, so <code>deleteWebhook</code> IPv6 stalls no longer trigger slow multi-account retry storms. Fixes #73255. Thanks @ttomiczek and @sktbrd.</li>
|
||||
<li>Gateway/agents: accept heartbeat, cron, and webhook as internal channel hints for agent runs so <code>sessions_spawn</code> works from non-delivery parent sessions while unknown channel hints still fail closed. Fixes #73237. Thanks @KeWang0622.</li>
|
||||
<li>Gateway/models: merge explicit <code>models.providers.*.models</code> rows into the Gateway model catalog with normalized provider/model dedupe, and use normalized image-capability lookup so custom vision models keep native image attachments even when Pi discovery omits them or model ID casing differs. Fixes #64213 and #65165. Thanks @billonese and @202233a.</li>
|
||||
<li>Gateway/reload: publish canonical post-write source config to in-process reloaders so simple config saves no longer create phantom plugin diffs or trigger unnecessary Gateway restarts. (#73267) Thanks @szsip239.</li>
|
||||
<li>Gateway/Docker: keep config-triggered restarts in-process inside containers instead of spawning a detached child and exiting PID 1 cleanly, so Docker Swarm and other on-failure supervisors do not leave the service stuck at 0/1 replicas. Fixes #73178. Thanks @du-nguyen-IT007.</li>
|
||||
<li>CLI/tasks: ship the task-registry control runtime in npm packages so <code>openclaw tasks cancel</code> can load ACP/subagent cancellation helpers from published builds. Fixes #68997. Thanks @1OAKDesign.</li>
|
||||
<li>Channels/Telegram: preserve unsent generated media after partial reply streaming has already delivered the text, so <code>image_generate</code> outputs still reach Telegram as photos instead of being dropped from the final payload. Fixes #73253. Thanks @mlaihk.</li>
|
||||
<li>Memory-core/dreaming: cap detached Dream Diary narrative subagents across cron sweeps so multi-workspace dreaming no longer fans out unbounded subagent sessions, lock contention, and cascading narrative timeouts. Fixes #73198. (#73287) Thanks @KeWang0622.</li>
|
||||
<li>CLI/agents: close local one-shot Claude live stdio sessions and bundled MCP loopback resources after embedded <code>openclaw agent --local</code> runs, while keeping gateway-owned MCP loopback cleanup internal to the Gateway. Thanks @frankekn.</li>
|
||||
<li>Export/session: keep inline export HTML scripts and vendor libraries injected after template formatting so generated session exports open with the app code, markdown renderer, and syntax highlighter present. Fixes #41862 and #49957; carries forward #41861 and #68947. Thanks @briannewman, @martenzi, and @armanddp.</li>
|
||||
<li>Agents/ACPX: stage the patched Claude ACP adapter as an ACPX runtime dependency and route known Codex/Claude ACP commands through local wrappers, so Gateway runtime no longer depends on live <code>npx</code> adapter resolution. Fixes #73202. Thanks @joerod26.</li>
|
||||
<li>Memory/compaction: let pre-compaction memory flush use an exact <code>agents.defaults.compaction.memoryFlush.model</code> override such as <code>ollama/qwen3:8b</code> without inheriting the active session fallback chain, so local housekeeping can avoid paid conversation models. Fixes #53772. Thanks @limen96.</li>
|
||||
<li>macOS/update: stop managed Gateway services before package replacement and keep LaunchAgent service secrets out of world-readable plist metadata by loading them from owner-only env files. Fixes #72996. Thanks @Mathewb7.</li>
|
||||
<li>Google Meet: keep observe-only Chrome joins and setup checks from requiring BlackHole or audio bridge commands, avoid granting or selecting the microphone in observe-only mode, and make <code>test_speech</code> report fresh realtime output-byte verification instead of only confirming a queued utterance. Refs #72478. Thanks @DougButdorf.</li>
|
||||
<li>Gateway/hooks: route non-delivered hook completion and error summaries to the target agent's main session instead of the default agent session, preserving multi-agent hook isolation. Fixes #24693; carries forward #68667. Thanks @abersonFAC and @bluesky6868.</li>
|
||||
<li>Control UI/models: request the configured Gateway model-list view so dashboards with only <code>models.providers.*.models</code> show those configured models first instead of flooding the picker with the full built-in catalog. Fixes #65405. Thanks @wbyanclaw.</li>
|
||||
<li>CLI/models: keep default-model and allowlist pickers on explicit <code>models.providers.*.models</code> entries when <code>models.mode</code> is <code>replace</code> instead of loading the full built-in catalog. Fixes #64950. Thanks @mrozentsvayg.</li>
|
||||
<li>Media/security: tighten media-understanding MIME sanitization so parameterized MIME values stay end-anchored and malformed whitespace or suffix payloads are rejected before file-context handling. Fixes #9795; carries forward #68225 with related review/test context from #61016/#68456. Thanks @ymaxgit, @bluesky6868, and @shamsulalam1114.</li>
|
||||
<li>Discord: own the Carbon interaction listener and hand off Discord slash/component handling asynchronously, so compaction or long session locks no longer trip <code>InteractionEventListener</code> listener timeouts. Fixes #73204. Thanks @slideshow-dingo.</li>
|
||||
<li>Compaction/diagnostics: keep unknown compaction failure classifications stable while logging sanitized detail for unclassified provider errors such as missing Ollama provider adapters. Thanks @gzsiang.</li>
|
||||
<li>Models/fallbacks: record first-class <code>model.fallback_step</code> trajectory events with from/to models, failure detail, chain position, and final outcome so support exports preserve the primary model failure even when a later fallback also fails. Fixes #71744. Thanks @nikolaykazakovvs-ux.</li>
|
||||
<li>Gateway/agents: block agent <code>exec</code> from launching interactive <code>openclaw channels login</code> flows and abort active agent runs after invalid-config recovery restores last-known-good config, preventing known channel-login and reload paths from wedging replies. Refs #72338. Thanks @midhunmonachan.</li>
|
||||
<li>Gateway/diagnostics: emit payload-free liveness warnings with event-loop delay, event-loop utilization, CPU-core ratio, active-session counts, and OTEL warning metrics/spans so live-but-stalled Gateways capture CPU-spin context in stability bundles and telemetry. Refs #72338. Thanks @midhunmonachan and @DougButdorf.</li>
|
||||
<li>Gateway/startup: keep value-option foreground starts on the gateway fast path and skip proxy bootstrap unless proxy env is configured, reducing normal gateway startup RSS and avoiding full CLI graph loading. Thanks @vincentkoc.</li>
|
||||
<li>Heartbeat/models: show heartbeat model bleed guidance on context-overflow resets when the last runtime model matches configured <code>heartbeat.model</code>, so smaller local heartbeat models point users to <code>isolatedSession</code> or <code>lightContext</code> instead of only compaction-buffer tuning. Fixes #67314. Thanks @Knightmare6890.</li>
|
||||
<li>Subagents/models: persist <code>sessions_spawn.model</code> and configured subagent models as child-session model overrides before the first turn, so spawned subagents actually run on the requested provider/model instead of reverting to the target agent default. Fixes #73180. Thanks @danielzinhu99.</li>
|
||||
<li>Channels/Telegram: keep webhook-mode local listeners alive and retry Telegram <code>setWebhook</code> registration after recoverable startup network failures, so transient Bot API timeouts no longer leave reverse proxies pointing at a closed listener. Fixes #71834. Thanks @jinon86.</li>
|
||||
<li>Agents/ACPX: bundle the Codex ACP adapter and launch it from the isolated <code>CODEX_HOME</code> wrapper before falling back to npm, so Codex ACP startup no longer depends on live <code>npx</code> resolution or the stale <code>@zed-industries/codex-acp@^0.11.1</code> range. Fixes #72037; refs #73202. Thanks @jasonftl, @sazora, and @joerod26.</li>
|
||||
<li>Agents/ACPX: register the embedded ACP backend at Gateway startup through a lightweight ACP backend SDK path and without importing the heavy ACPX runtime until an ACP session or explicit startup probe needs it, reducing baseline Gateway RSS. Thanks @vincentkoc.</li>
|
||||
<li>CLI/update: keep restart health polling when the restarted Gateway is reachable but has not reported its version yet, so macOS service restarts do not fail early with <code>actual unavailable</code>. Thanks @ProspectOre.</li>
|
||||
<li>Backup: skip installed plugin <code>extensions/*/node_modules</code> dependency trees while keeping plugin manifests and source files in archives, so local backups avoid rebuildable npm payload bloat. Fixes #64144. Thanks @BrilliantWang.</li>
|
||||
<li>Cron/models: fail isolated cron runs closed when an explicit <code>payload.model</code> is not allowed or cannot be resolved, so scheduled jobs do not silently fall back to an unrelated agent default or paid route before configured provider proxies such as LiteLLM can run. Fixes #73146. Thanks @oneandrewwang.</li>
|
||||
<li>Memory/QMD: back off repeated chat-turn QMD open failures while still letting memory status and CLI probes recheck immediately, so a broken sidecar dependency cannot trigger active-memory or cron retry storms. Fixes #73188 and #73176. Thanks @leonlushgit and @w3i-William.</li>
|
||||
<li>Talk Mode: resolve <code>messages.tts.providers.<id>.apiKey</code> through the active runtime snapshot for <code>talk.config</code>, so Talk overlays can discover SecretRef-backed speech providers without falling back to local speech. Fixes #73109. (#73111) Thanks @omarshahine.</li>
|
||||
<li>Memory/Ollama: resolve <code>memorySearch.provider</code> custom provider ids through their configured <code>models.providers.<id>.api</code> owner, so multi-GPU Ollama setups can dedicate embeddings to providers such as <code>ollama-5080</code> without losing the Ollama adapter or local auth semantics. Fixes #73150. Thanks @oneandrewwang.</li>
|
||||
<li>CLI/memory: skip eager context-window warmup for <code>openclaw memory</code> commands so memory search does not race unrelated model metadata discovery. Fixes #73123. Thanks @oalansilva and @neeravmakwana.</li>
|
||||
<li>CLI/Telegram: route Telegram <code>message send</code> and poll actions through the running Gateway when available, so packaged installs use the staged <code>grammy</code> runtime deps and CLI sends return instead of hanging after the Telegram channel is active. Fixes #73140. Thanks @oalansilva.</li>
|
||||
<li>Plugins/runtime deps: prepare staged bundled plugin dependencies before loading packaged public surfaces, so OpenClaw's Telegram runtime/test facade loads resolve <code>grammy</code> from the managed runtime-deps stage without copying dependencies into the global package root. Refs #73140. Thanks @oalansilva.</li>
|
||||
<li>Agents/exec: emit <code>(no output)</code> for silent exec update and node-host result blocks so Anthropic-compatible providers no longer reject empty tool-result text after quiet commands. Fixes #73117. Thanks @pfrederiksen and @Sanjays2402.</li>
|
||||
<li>Cron/providers: preflight local Ollama and OpenAI-compatible provider endpoints before isolated cron agent turns, record unreachable local providers as skipped runs, and cache dead-endpoint probes so many jobs do not hammer the same stopped local server. Fixes #58584. Thanks @jpeghead.</li>
|
||||
<li>Gateway/config: let config reload continue in degraded mode when invalidity is scoped to plugin entries, so incompatible plugin configs can be skipped and the Gateway restart can still pick up the rest of the config after rollbacks. Fixes #73131. Thanks @Adam-Researchh.</li>
|
||||
<li>Doctor/channels: suppress disabled bundled-plugin blocker warnings when a trusted external plugin owns the configured channel, so Lark/Feishu installs no longer get Feishu repair noise after switching to <code>openclaw-lark</code>. Fixes #56794. Thanks @wuji-tech-dev.</li>
|
||||
<li>CLI/status: show skipped fast-path memory checks as <code>not checked</code> and report active custom memory plugin runtime status from <code>status --json --all</code> without requiring built-in <code>agents.defaults.memorySearch</code>, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius.</li>
|
||||
<li>Gateway/channels: record and log unexpected clean channel monitor exits so channels that return without throwing no longer appear stopped with no error. Fixes #73099. Thanks @balaji1968-kingler.</li>
|
||||
<li>Discord/group chats: keep group/channel replies private by default unless the agent explicitly uses the message tool, so always-on rooms can lurk without leaking automatic final, block, preview, or status-reaction output; <code>messages.groupChat.visibleReplies: "automatic"</code> restores legacy auto-posting. (#73046) Thanks @scoootscooob.</li>
|
||||
<li>Plugins/package: force nested bundled-plugin runtime dependency installs out of inherited npm dry-run mode during prepack and package smoke checks, so packed installs materialize required plugin modules instead of reporting missing bundled files. Refs #73128. Thanks @Adam-Researchh.</li>
|
||||
<li>Discord: skip reaction events before REST channel fetch when notifications are off, guild reactions are disabled, or allowlist mode cannot match without channel overrides, reducing reconnect bursts that caused slow listener warnings. Fixes #73133. Thanks @isaacsummers.</li>
|
||||
<li>Channels/Telegram: centralize polling update tracking so accepted offsets remain durable across restarts, same-process handler failures can still retry, and slow offset writes cannot overwrite newer accepted watermarks. Refs #73115. Thanks @vdruts.</li>
|
||||
<li>Agents/models: classify empty, reasoning-only, and planning-only terminal agent runs before accepting a model fallback candidate, so invalid or incompatible models can advance to the next configured fallback instead of returning a 30-second terminal failure. Fixes #73115. Thanks @vdruts.</li>
|
||||
<li>Memory/LanceDB: let embedding config use provider-backed auth profiles, environment credentials, or provider config without a separate plugin <code>embedding.apiKey</code>, so OAuth-capable embedding providers can power auto-recall/capture. Fixes #68950. Thanks @malshaalan-ai.</li>
|
||||
<li>CLI/parents: invoking <code>openclaw <parent></code> (memory, channels, plugins, approvals, devices, cron, mcp) without a subcommand now prints the parent's help and exits <code>0</code>, matching <code><parent> --help</code> and the existing <code>agents</code> / <code>sessions</code> defaults so shell <code>&&</code> chains and pnpm wrappers no longer surface a misleading <code>ELIFECYCLE Command failed with exit code 1.</code> line. Fixes #73077. Thanks @hclsys.</li>
|
||||
<li>Plugins/hooks: time out never-settling <code>agent_end</code> observation hooks after 30 seconds and log the plugin failure, so hung embedding endpoints no longer leave memory capture silently pending forever. Fixes #65544. Thanks @ghoc0099.</li>
|
||||
<li>Gateway/config: serve runtime config schemas from the current plugin metadata snapshot and generated bundled channel schema metadata instead of rebuilding plugin channel config modules on every <code>config.get</code>/<code>config.schema</code>, preventing idle plugin-discovery CPU churn after upgrades. Fixes #73088. Thanks @sleitor and @geovansb.</li>
|
||||
<li>Memory/LanceDB: call OpenAI-compatible embedding endpoints through the raw SDK transport without sending <code>encoding_format</code>, then normalize float-array or base64 responses so providers such as ZhiPu and DashScope no longer fail recall with wrong vector dimensions or rejected parameters. Fixes #63655. Thanks @kinthaiofficial.</li>
|
||||
<li>Plugins/install: run dependency installs with npm error-level logging instead of silent mode so failed plugin or hook installs surface actionable npm errors such as EUNSUPPORTEDPROTOCOL instead of <code>npm install failed:</code> with no detail. (#73093) Thanks @sanctrl.</li>
|
||||
<li>Memory/LanceDB: bound memory recall embedding queries with a new <code>recallMaxChars</code> setting, prefer the latest user message over channel prompt metadata during auto-recall, and document the knob so small Ollama embedding models avoid context-length failures. Fixes #56780. Thanks @rungmc357 and @zak-collaborator.</li>
|
||||
<li>CLI/skills: resolve workspace-backed skills commands from <code>--agent</code>, then the current agent workspace, before falling back to the default agent, so multi-agent ClawHub installs, updates, and status checks stay scoped to the active workspace. Fixes #56161; carries forward #72726. Thanks @langbowang and @luyao618.</li>
|
||||
<li>Plugin SDK: fall back from partial bundled plugin directory overrides to package source public surfaces while preserving <code>OPENCLAW_DISABLE_BUNDLED_PLUGINS</code> as a hard disable. (#72817) Thanks @serkonyc.</li>
|
||||
<li>Agents/ACPX: stop forwarding Codex ACP timeout config controls that Codex rejects while preserving OpenClaw's run-timeout watchdog for ACP subagents. Fixes #73052. Thanks @pfrederiksen and @richa65.</li>
|
||||
<li>Memory Core: stream fallback vector search scoring with a bounded top-K result set so large indexes do not materialize every chunk embedding when sqlite-vec is unavailable. (#73069) Thanks @parkertoddbrooks.</li>
|
||||
<li>Memory Core: stream embedding-cache seeding during safe reindex so large local caches do not materialize every row into the V8 heap before the atomic rebuild. (#73067) Thanks @parkertoddbrooks.</li>
|
||||
<li>Memory/Ollama: add <code>memorySearch.remote.nonBatchConcurrency</code> for inline embedding indexing, default Ollama non-batch indexing to one request at a time, and keep batch concurrency separate from non-batch concurrency so local embedding backfills avoid timeout storms on smaller hosts. Carries forward #57733. Thanks @itilys.</li>
|
||||
<li>macOS app: update Peekaboo, ElevenLabsKit, and MLX TTS helper dependencies, make canvas file watching and config/exec-approval state writes reliable under concurrent app/test activity, and keep the app plus helper builds warning-free. Thanks @Blaizzy.</li>
|
||||
<li>iOS app: refresh SwiftPM/XcodeGen source hygiene, make app, extension, watch, and curated shared Swift files pass the prebuild SwiftFormat and SwiftLint checks, move relay registration off deprecated StoreKit receipt APIs, and keep simulator builds and logic tests warning-free. Thanks @ngutman.</li>
|
||||
<li>Agents/models: keep <code>models.json</code> readiness and provider-hook caches warm across repeated agent and subagent model resolution while preserving external <code>models.json</code> invalidation, reducing repeated provider-plugin loads on slower ARM64 hosts. Fixes #73075. Thanks @jochen.</li>
|
||||
<li>Docs/tools: clarify that <code>tools.profile: "messaging"</code> is intentionally narrow and that <code>tools.profile: "full"</code> is the unrestricted baseline for broader command/control access. Carries forward #39954. Thanks @posigit.</li>
|
||||
<li>Control UI/Agents: redact tool-call args, partial/final results, derived exec output, and configured custom secret patterns before streaming tool events to the Control UI, so tool output cannot expose provider or channel credentials. Fixes #72283. (#72319) Thanks @volcano303 and @BunsDev.</li>
|
||||
<li>Agents/sessions: keep <code>sessions_history</code> recall redaction enabled even when general log redaction is disabled, and clarify that safety-boundary UI/tool/diagnostic payloads still redact independently of <code>logging.redactSensitive</code>. Carries forward #72319. Thanks @volcano303 and @BunsDev.</li>
|
||||
<li>Providers/Codex: pass agent and workspace directories into provider stream wrappers so Codex native <code>web_search</code> activation can evaluate the correct auth context, and smoke-test the built status-message runtime by resolving the emitted bundle name. Carries forward #67843; refs #65909. Thanks @neilofneils404.</li>
|
||||
<li>Cron/models: keep <code>payload.model</code> as a per-job primary that can use configured fallbacks, while still letting <code>payload.fallbacks: []</code> make cron runs strict and avoid hidden agent-primary retries. Refs #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>Models/fallbacks: treat user-selected session models as exact choices, so <code>/model ollama/...</code> and model-picker switches fail visibly when the selected provider is unreachable instead of answering from an unrelated configured fallback. Fixes #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>Codex harness: keep ChatGPT subscription app-server runs from inheriting <code>CODEX_API_KEY</code> or <code>OPENAI_API_KEY</code>, and fall back to <code>CODEX_API_KEY</code> / <code>OPENAI_API_KEY</code> app-server login only when no Codex account is available. Fixes #73057. Thanks @holgergruenhagen and @pashpashpash.</li>
|
||||
<li>CLI/model probes: fail local <code>infer model run</code> probes when the provider returns no text output, so unreachable local providers and empty completions no longer look like successful smoke tests. Refs #73023. Thanks @pavelyortho-cyber.</li>
|
||||
<li>CLI/Ollama: run local <code>infer model run</code> through the lean provider completion path and skip global model discovery for one-shot local probes, so Ollama smoke tests no longer pay full chat-agent/tool startup cost or hang before the native <code>/api/chat</code> request. Fixes #72851. Thanks @TotalRes2020.</li>
|
||||
<li>Doctor/gateway services: ignore launchd/systemd companion services that only reference the gateway as a dependency, suppress inactive Linux extra-service warnings, and avoid rewriting a running systemd gateway command/entrypoint during doctor repair. Carries forward #39118. Thanks @therk.</li>
|
||||
<li>Daemon/service: only emit hard-coded version-manager paths such as <code>~/.volta/bin</code>, <code>~/.asdf/shims</code>, <code>~/.bun/bin</code>, and fnm/pnpm fallbacks into gateway and node service PATHs when the directories exist, so <code>openclaw doctor</code> no longer flags <code>gateway.path.non-minimal</code> against a PATH the daemon just wrote. Env-driven roots and stable user-bin dirs remain unconditional. Fixes #71944; carries forward #71964. Thanks @Sanjays2402.</li>
|
||||
<li>CLI/startup: disable Node's module compile cache automatically for live source-checkout launchers so in-place <code>pnpm build</code> updates are visible to the next <code>openclaw</code> CLI invocation. Fixes #73037. Thanks @LouisGameDev.</li>
|
||||
<li>Agents/group chat: keep silent-allowed empty and reasoning-only turns on the <code>NO_REPLY</code> path without injecting visible-answer retry prompts, and clarify the group prompt so agents use the exact silent token instead of prose. Thanks @vincentkoc.</li>
|
||||
<li>Agents/group chat: move <code>NO_REPLY</code> mechanics into channel-aware direct/group prompts and suppress the duplicate generic silent-reply section for auto-reply runs, so always-on group agents get one consistent stay-silent instruction. Thanks @vincentkoc.</li>
|
||||
<li>Providers/OpenAI: preserve encrypted empty-summary Responses reasoning items in WebSocket replay and request <code>reasoning.encrypted_content</code> on reasoning turns so GPT-5.4/GPT-5.5 sessions do not lose required <code>rs_*</code> state beside <code>msg_*</code> items. Fixes #73053. Thanks @odb36777.</li>
|
||||
<li>Gateway/startup: treat <code>plugins.enabled=false</code> as an early plugin fast path, skipping plugin auto-enable discovery, gateway plugin lookup/runtime-dependency staging, and stale-plugin cleanup warnings while preserving channel blocker warnings. (#73041) Thanks @WuKongAI-CMU.</li>
|
||||
<li>Channels/commands: make generated <code>/dock-*</code> commands switch the active session reply route through <code>session.identityLinks</code> instead of falling through to normal chat. Fixes #69206; carries forward #73033. Thanks @clawbones and @michaelatamuk.</li>
|
||||
<li>Providers/Cloudflare AI Gateway: strip assistant prefill turns from Anthropic Messages payloads when thinking is enabled, so Claude requests through Cloudflare AI Gateway no longer fail Anthropic conversation-ending validation. Fixes #72905; carries forward #73005. Thanks @AaronFaby and @sahilsatralkar.</li>
|
||||
<li>Gateway/startup: keep primary-model startup prewarm on scoped metadata preparation, let native approval bootstraps retry outside channel startup, and skip the global hook runner when no <code>gateway_start</code> hook is registered, so clean post-ready sidecar work stays off the critical path. Refs #72846. Thanks @RayWoo, @livekm0309, and @mrz1836.</li>
|
||||
<li>Gateway/channels: start bundled channel accounts with a lightweight <code>runtimeContexts</code> surface instead of importing the full reply/routing/session channel runtime before <code>startAccount</code>, so Discord, Telegram, Slack, Matrix, and QQBot startup no longer block on unrelated channel helper graphs. Refs #72846 and #72960. Thanks @mrz1836, @RayWoo, and @rollingshmily.</li>
|
||||
<li>Gateway/supervisor: exit cleanly when a supervised restart finds an existing healthy gateway and bound retries when the existing gateway stays unhealthy, so stale lock contention cannot loop indefinitely. Refs #72846. Thanks @azgardtek.</li>
|
||||
<li>Gateway/startup: scope primary-model provider discovery during channel prewarm to the configured provider owner and add split startup trace timings, so boot avoids staging unrelated bundled provider dependencies while setup discovery remains broad. Fixes #73002. Thanks @Schnup03.</li>
|
||||
<li>Plugins/runtime deps: declare retained staged bundled plugin dependencies in the npm staging manifest while installing only newly missing packages, so Gateway restarts avoid reinstalling the full retained dependency set when one runtime dependency is absent. Fixes #73055. Thanks @GCorp2026.</li>
|
||||
<li>CLI/status: keep default <code>openclaw status</code> off the heavyweight security audit, plugin compatibility, and memory-vector probes while still showing configured Telegram channels through setup metadata, so routine health checks stay fast and no longer render an empty Channels table. Fixes #72993. Thanks @comick1.</li>
|
||||
<li>Channels/Telegram: send a best-effort native typing cue immediately after an inbound message is accepted, so slow pre-dispatch turns show Telegram liveness before queueing, compaction, model, or tool work starts. Fixes #63759. Thanks @alessandropcostabr.</li>
|
||||
<li>Channels/Telegram: stop native approval startup auth failures from retrying every second, while still waiting through retryable Gateway auth handoffs, so Telegram approval setup problems no longer create a reconnect/log loop during channel startup. Refs #72846 and #72867. Thanks @kiranvk-2011 and @porly1985.</li>
|
||||
<li>Channels/Microsoft Teams: unwrap staged CommonJS JWT runtime dependencies before Bot Connector token validation so inbound Teams messages no longer 401 after the bundled runtime-deps move. Fixes #73026. Thanks @kbrown10000.</li>
|
||||
<li>Gateway/auth: allow local direct callers in trusted-proxy mode to use the configured gateway password as an internal fallback while keeping token fallback rejected. Fixes #17761. Thanks @dashed, @vincentkoc, and @jetd1.</li>
|
||||
<li>Gateway/auth: add explicit <code>trustedProxy.allowLoopback</code> support for same-host loopback reverse proxies while keeping loopback trusted-proxy auth fail-closed by default and preserving required-header and allowlist checks. Fixes #59167; carries forward #63379. Thanks @Matir, @jeremyakers, and @mrosmarin.</li>
|
||||
<li>Channels/sessions: prevent guarded inbound session recording from creating route-only phantom sessions while still allowing last-route updates for sessions that already exist. Carries forward #73009. Thanks @jzakirov.</li>
|
||||
<li>Cron: accept <code>delivery.threadId</code> in Gateway cron add/update schemas so scheduled announce delivery can target Telegram forum topics and other threaded channel destinations through the documented delivery path. Fixes #73017. Thanks @coachsootz.</li>
|
||||
<li>Plugins/runtime deps: stage bundled plugin dependencies imported by mirrored root dist chunks, so packaged memory and status commands do not miss <code>chokidar</code> or similar root-chunk dependencies after update. Fixes #72882 and #72970; carries forward #72992. Thanks @shrimpy8, @colin-chang, and @Schnup03.</li>
|
||||
<li>Plugins/runtime deps: reuse unchanged bundled plugin runtime mirrors instead of rebuilding plugin trees on every load, cutting avoidable writes and restart/reconnect I/O on slow storage. Fixes #72933. Thanks @jasonftl.</li>
|
||||
<li>Agents/runtime context: deliver hidden runtime context through prompt-local system context while keeping the transcript-only custom entry out of provider user turns, and strip stale copied runtime-context prefaces from user-facing replies. Fixes #72386; carries forward #72969. Thanks @jhsmith409.</li>
|
||||
<li>Channels/Telegram: skip the optional webhook-info API call during polling-mode status checks and startup bot-label probes so long-polling setups avoid an unnecessary Telegram round trip. Carries forward #72990. Thanks @danielgruneberg.</li>
|
||||
<li>CLI/message: resolve targeted <code>openclaw message</code> channels to their owning plugin before loading the registry, and fall back to configured channel plugins when the channel must be inferred, so scripted sends avoid full bundled plugin registry scans without assuming channel ids match plugin ids. Fixes #73006. Thanks @jasonftl.</li>
|
||||
<li>Plugins/startup: parse strict JSON plugin manifests with native JSON first and keep JSON5 as the compatibility fallback, reducing manifest registry CPU during Gateway boot and CLI startup. Fixes #73011. Thanks @jasonftl.</li>
|
||||
<li>CLI/models: keep route-first <code>models status --json</code> stdout reserved for the JSON payload by routing auth-profile and startup diagnostics to stderr. Fixes #72962. Thanks @vishutdhar.</li>
|
||||
<li>Gateway/runtime: keep dirty-tree status calls from rebuilding live <code>dist</code>, clear stale task and restart state across in-process restarts, retry transient Discord lazy imports, and let channel startup continue after slow model warmup so browser, Discord, and voice-call sidecars come online. Thanks @vincentkoc.</li>
|
||||
<li>Security/CodeQL: replace file SecretRef id gateway schema regex validation with segment-aligned predicates and set empty permissions on release summary/backfill jobs so the narrowed CodeQL profile stays clean. Thanks @vincentkoc.</li>
|
||||
<li>Sessions: ignore future-dated session activity timestamps during reset freshness checks and cap future <code>updatedAt</code> values at the merge boundary so clock-skewed messages cannot keep stale sessions alive forever. Fixes #72989. Thanks @martingarramon.</li>
|
||||
<li>Sessions: apply search, activity filters, and limits before gateway row enrichment so bounded session lists avoid scanning discarded transcripts. Carries forward #72978. Thanks @yeager.</li>
|
||||
<li>Sessions: remove trajectory runtime and pointer sidecars when session maintenance prunes, caps, or disk-evicts their owning session, while preserving sidecars still referenced by live rows. Fixes #73000. Thanks @jared-rebel.</li>
|
||||
<li>Plugins/CLI: allow managed plugin installs when the active extensions root is a symlink to a real state directory, while keeping nested target symlinks blocked and suppressing misleading hook-pack fallback errors for install-boundary failures. Fixes #72946. Thanks @mayank6136.</li>
|
||||
<li>Providers/Ollama: mark discovered Ollama catalog models as supporting streaming usage metadata so token accounting stays enabled for local models. (#72976) Thanks @sdeyang.</li>
|
||||
<li>Media understanding: reject malformed MIME values with trailing junk while preserving standard parameter tails before enrichment uses them. (#72914) Thanks @volcano303.</li>
|
||||
<li>WebChat: keep bare <code>/new</code> and <code>/reset</code> prompts from producing empty transcript text by inserting the hidden session marker when the visible tail is blank. (#72863) Thanks @mahopan.</li>
|
||||
<li>CLI/update: explain completion-cache refresh timeouts with manual refresh guidance instead of surfacing a raw low-level timeout. Fixes #72842. (#72850) Thanks @iot2edge.</li>
|
||||
<li>Memory-core/dreaming: give narrative generation a 60-second timeout so slower local or remote models can finish instead of timing out at 15 seconds. Fixes #72837. (#72852) Thanks @RayWoo.</li>
|
||||
<li>Plugins/hooks: inject each plugin's resolved config into internal hook event context without mutating the shared event object. (#72888) Thanks @jalapeno777.</li>
|
||||
<li>Agents/ACP: pass the resolved ACP agent directory into media understanding so per-agent media caches and config are used for ACP-dispatched image turns. (#72832) Thanks @luyao618.</li>
|
||||
<li>Gateway/Bonjour: truncate mDNS service names and host labels to the 63-byte DNS label limit at valid UTF-8 boundaries. (#72809) Thanks @luyao618.</li>
|
||||
<li>Feishu: treat groups explicitly configured under channels.feishu.groups as admitted even when groupAllowFrom is empty, while preserving groupPolicy: "disabled" as a hard group block and keeping groups.\* wildcard defaults non-admitting. Fixes #67687. (#72789) Thanks @MoerAI.</li>
|
||||
<li>Gateway/startup: keep hot Gateway boot paths on leaf config imports and add max-RSS reporting to the gateway startup bench so low-memory startup regressions are visible before release. Thanks @vincentkoc.</li>
|
||||
<li>WebChat: read <code>chat.history</code> from active transcript branches, drop stale streamed assistant tails once final history catches up, and coalesce duplicate in-flight Control UI submits, so rewritten prompts, completed replies, and rapid send events no longer render or process twice. Fixes #72975, #72963, and #72974. Thanks @dmagdici, @lhtpluto, and @Benjamin5281999.</li>
|
||||
<li>WebChat/TTS: persist automatic final-mode TTS audio as a supplemental audio-only transcript update instead of adding a second assistant message with the same visible text. Fixes #72830. Thanks @lhtpluto.</li>
|
||||
<li>Agents/LSP: terminate bundled stdio LSP process trees during runtime disposal and Gateway shutdown, so nested children such as <code>tsserver</code> do not survive stop or restart. Fixes #72357. Thanks @ai-hpc and @bittoby.</li>
|
||||
<li>Diagnostics/OTEL: capture privacy-safe model-call request payload bytes, streamed response bytes, first-response latency, and total duration in diagnostic events, plugin hooks, stability snapshots, and OTEL model-call spans/metrics without logging raw model content. Fixes #33832. Thanks @wwh830.</li>
|
||||
<li>Logging: write validated diagnostic trace context as top-level <code>traceId</code>, <code>spanId</code>, <code>parentSpanId</code>, and <code>traceFlags</code> fields in file-log JSONL records so traced requests and model calls are easier to correlate in log processors. Refs #40353. Thanks @liangruochong44-ui.</li>
|
||||
<li>Logging/sessions: apply configured redaction patterns to persisted session transcript text and accept escaped character classes in safe custom redaction regexes, so transcript JSONL no longer keeps matching sensitive text in the clear. Fixes #42982. Thanks @panpan0000.</li>
|
||||
<li>Providers/Ollama: honor <code>/api/show</code> capabilities when registering local models so non-tool Ollama models no longer receive the agent tool surface, and keep native Ollama thinking opt-in instead of enabling it by default. Fixes #64710 and duplicate #65343. Thanks @yuan-b, @netherby, @xilopaint, and @Diyforfun2026.</li>
|
||||
<li>Control UI/Agents: remount the Overview model controls when switching agents so the primary-model picker cannot retain stale per-agent selection. Fixes #39392; carries forward #39401, notes the duplicate #39495 approach, and keeps #46275/#54724 broader stabilization out of scope. Thanks @daijunyi002, @SergioChan, @aworki, and @wsyjh8.</li>
|
||||
<li>Auto-reply: poison inbound message dedupe after replay-unsafe provider/runtime failures so retries stay safe before visible progress but cannot duplicate messages after block output, tool side effects, or session progress. Fixes #69303; keeps #58549 and #64606 as duplicate validation. Thanks @martingarramon, @NikolaFC, and @zeroth-blip.</li>
|
||||
<li>Agents/model fallback: jump directly to a known later live-session model redirect instead of walking unrelated fallback candidates, while preserving the already-landed live-session/fallback loop guard. Fixes #57471; related loop family already closed via #58496. Thanks @yuxiaoyang2007-prog.</li>
|
||||
<li>Gateway/Bonjour: keep @homebridge/ciao cancellation handlers registered across advertiser restarts so late probing cancellations cannot crash Linux and other mDNS-churned gateways. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/startup: load the default <code>memory-core</code> slot during Gateway startup when permitted so active-memory recall can call <code>memory_search</code> and <code>memory_get</code> without requiring an explicit <code>plugins.slots.memory</code> entry, while preserving <code>plugins.slots.memory: "none"</code>. Thanks @vincentkoc.</li>
|
||||
<li>Gateway/plugins: resolve <code>gateway_start</code> cron hooks from live Gateway runtime state before the legacy deps fallback, so memory-core dreaming cron reconciliation keeps working on installs where <code>deps.cron</code> is not populated during service startup. Fixes #72835. Thanks @RayWoo.</li>
|
||||
<li>Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet.</li>
|
||||
<li>Plugins/compat: inventory doctor-side deprecation migrations separately from runtime plugin compatibility so release sweeps preserve needed repairs while enforcing dated removal windows. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale <code>plugins list</code> entries. Thanks @vincentkoc.</li>
|
||||
<li>Plugins/CLI: make plugin install and uninstall config writes conflict-aware, clear stale denylist entries on explicit reinstall/removal, and delete managed plugin files only after config/index commit succeeds. Thanks @vincentkoc.</li>
|
||||
<li>Plugins: fail <code>plugins update</code> when tracked plugin or hook updates error, keep bundled runtime-dependency repair behind restrictive allowlists, and reject package installs with unloadable extension entries. Thanks @vincentkoc.</li>
|
||||
<li>WebChat/Control UI: support non-video file attachments in chat uploads while preserving the existing image attachment path and MIME-sniff fallback for generic image uploads. (#70947) Thanks @IAMSamuelRodda.</li>
|
||||
<li>Skills/memory: restore Chokidar v5 hot reloads by watching concrete skill and memory roots with filters, including SKILL.md removals and deleted skill folders without broad workspace recursion. Fixes #27404, #33585, and #41606. Thanks @shelvenzhou, @08820048, and @rocke2020.</li>
|
||||
<li>Gateway/chat: keep duplicate attachment-backed <code>chat.send</code> retries with the same idempotency key on the documented in-flight path so aborts still target the real active run. Fixes #70139. Thanks @Feelw00.</li>
|
||||
<li>Gateway/chat: preserve repeated boundary characters while merging assistant chat stream deltas, including repeated digits, CJK characters, and markdown/table tokens. Fixes #63769; carries forward #63994 and #65457. Thanks @yon950905 and @mohuaxiao.</li>
|
||||
<li>Plugins: share package entrypoint resolution between install and discovery, reject mismatched <code>runtimeExtensions</code>, and cache bundled runtime-dependency manifest reads during scans. Thanks @vincentkoc.</li>
|
||||
<li>WhatsApp/Web: keep quiet but healthy linked-device sessions connected by basing the watchdog on WhatsApp Web transport activity, while retaining a longer app-silence cap so frame activity cannot mask a stuck session forever. Fixes #70678; carries forward the focused #71466 approach and keeps #63939 as related configurable-timeout follow-up. Thanks @vincentkoc and @oromeis.</li>
|
||||
<li>Discord/gateway: count failed health-monitor restart attempts toward cooldown and hourly caps, and evict stale account lifecycle state during channel reloads so repeated Discord gateway recovery cannot loop on old status. Fixes #38596. (#40413) Thanks @jellyAI-dev and @vashquez.</li>
|
||||
<li>TTS/BlueBubbles: pre-transcode synthesized MP3 audio to opus-in-CAF (mono, 24 kHz — validated against macOS 15.x Messages.app's native voice-memo CAF descriptor) on macOS hosts before handing the file to BlueBubbles, so iMessage renders the result as a native voice-memo bubble with proper duration and waveform UI instead of a plain file attachment. Adds an opt-in <code>tts.voice.preferAudioFileFormat</code> channel capability and a magic-byte sniff for the CAF container so the host-local-media validator (which uses <code>file-type</code> and didn't recognize CAF natively) can verify the pre-transcoded buffer. Channels that don't opt in are unaffected. (#72586) Fixes #72506. Thanks @omarshahine.</li>
|
||||
<li>Feishu: retry WebSocket startup failures with monitor-owned backoff while preserving SDK-local heartbeat defaults, so persistent-connection startup failures no longer leave the monitor hung. Fixes #68766; related #42354 and #55532. Thanks @alex-xuweilong, @120106835, @sirfengyu, and @tianhaocui.</li>
|
||||
</ul>
|
||||
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.27/OpenClaw-2026.4.27.zip" length="50595360" type="application/octet-stream" sparkle:edSignature="X8DQNQNWVcvtpYLkhZcsKNpnA78ycyzgGlZaG0XBY1GIph3oZNUIpAszGGocJVqTK7+F89Au5ZPb60mOqJQ6DQ=="/>
|
||||
</item>
|
||||
<item>
|
||||
<title>2026.4.26</title>
|
||||
<pubDate>Tue, 28 Apr 2026 02:40:27 +0000</pubDate>
|
||||
@@ -933,5 +641,53 @@
|
||||
]]></description>
|
||||
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.4.25/OpenClaw-2026.4.25.zip" length="48125363" type="application/octet-stream" sparkle:edSignature="RnQ01wCFgupauUdwOFan+XPGZhBJi/w3sgJYA5EaasbeGrduDHBGw1e9Zj2Lqb4ud8e6Q+tRcJVfxh5KKSEIDg=="/>
|
||||
</item>
|
||||
<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>
|
||||
</channel>
|
||||
</rss>
|
||||
@@ -28,6 +28,7 @@ let talkPhaseSoundsEnabledKey = "openclaw.talkPhaseSoundsEnabled"
|
||||
let talkShiftToStopEnabledKey = "openclaw.talkShiftToStopEnabled"
|
||||
let iconOverrideKey = "openclaw.iconOverride"
|
||||
let connectionModeKey = "openclaw.connectionMode"
|
||||
let gatewayNativeHostEnabledKey = "openclaw.gatewayNativeHostEnabled"
|
||||
let remoteTargetKey = "openclaw.remoteTarget"
|
||||
let remoteIdentityKey = "openclaw.remoteIdentity"
|
||||
let remoteProjectRootKey = "openclaw.remoteProjectRoot"
|
||||
|
||||
@@ -92,6 +92,14 @@ struct DebugSettings: View {
|
||||
self.launchAgentWriteDisabled = GatewayLaunchAgentManager.isLaunchAgentWriteDisabled()
|
||||
return
|
||||
}
|
||||
if newValue {
|
||||
Task {
|
||||
_ = await GatewayLaunchAgentManager.set(
|
||||
enabled: false,
|
||||
bundlePath: Bundle.main.bundlePath,
|
||||
port: GatewayEnvironment.gatewayPort())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
|
||||
@@ -7,8 +7,14 @@ enum GatewayAutostartPolicy {
|
||||
|
||||
static func shouldEnsureLaunchAgent(
|
||||
mode: AppState.ConnectionMode,
|
||||
paused: Bool) -> Bool
|
||||
paused: Bool,
|
||||
defaults: UserDefaults = .standard,
|
||||
environment: [String: String] = ProcessInfo.processInfo.environment) -> Bool
|
||||
{
|
||||
self.shouldStartGateway(mode: mode, paused: paused)
|
||||
self.shouldStartGateway(mode: mode, paused: paused) &&
|
||||
!GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: mode,
|
||||
defaults: defaults,
|
||||
environment: environment)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,7 @@ enum GatewayLaunchAgentManager {
|
||||
private static let disableLaunchAgentMarker = ".openclaw/disable-launchagent"
|
||||
|
||||
private static var disableLaunchAgentMarkerURL: URL {
|
||||
#if DEBUG
|
||||
if let testingDisableLaunchAgentMarkerURL {
|
||||
return testingDisableLaunchAgentMarkerURL
|
||||
}
|
||||
#endif
|
||||
return FileManager().homeDirectoryForCurrentUser
|
||||
FileManager().homeDirectoryForCurrentUser
|
||||
.appendingPathComponent(self.disableLaunchAgentMarker)
|
||||
}
|
||||
|
||||
@@ -24,10 +19,6 @@ enum GatewayLaunchAgentManager {
|
||||
return false
|
||||
}
|
||||
|
||||
static func applyAttachOnlyRuntimeOverride() -> String? {
|
||||
self.setLaunchAgentWriteDisabled(true)
|
||||
}
|
||||
|
||||
static func setLaunchAgentWriteDisabled(_ disabled: Bool) -> String? {
|
||||
let marker = self.disableLaunchAgentMarkerURL
|
||||
if disabled {
|
||||
@@ -153,15 +144,6 @@ extension GatewayLaunchAgentManager {
|
||||
timeout: Double,
|
||||
quiet: Bool) async -> CommandResult
|
||||
{
|
||||
#if DEBUG
|
||||
if self.testingInterceptDaemonCommands {
|
||||
self.testingDaemonCommandCalls.append(args)
|
||||
return CommandResult(
|
||||
success: true,
|
||||
payload: Data("{\"ok\":true}".utf8),
|
||||
message: nil)
|
||||
}
|
||||
#endif
|
||||
let command = CommandResolver.openclawCommand(
|
||||
subcommand: "gateway",
|
||||
extraArgs: self.withJsonFlag(args),
|
||||
@@ -205,26 +187,4 @@ extension GatewayLaunchAgentManager {
|
||||
private static func summarize(_ text: String) -> String? {
|
||||
TextSummarySupport.summarizeLastLine(text)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
private nonisolated(unsafe) static var testingDisableLaunchAgentMarkerURL: URL?
|
||||
private nonisolated(unsafe) static var testingInterceptDaemonCommands = false
|
||||
private nonisolated(unsafe) static var testingDaemonCommandCalls: [[String]] = []
|
||||
|
||||
static func setTestingDisableLaunchAgentMarkerURL(_ url: URL?) {
|
||||
self.testingDisableLaunchAgentMarkerURL = url
|
||||
}
|
||||
|
||||
static func setTestingInterceptDaemonCommands(_ intercept: Bool) {
|
||||
self.testingInterceptDaemonCommands = intercept
|
||||
}
|
||||
|
||||
static func clearTestingDaemonCommandCalls() {
|
||||
self.testingDaemonCommandCalls.removeAll(keepingCapacity: false)
|
||||
}
|
||||
|
||||
static func testingDaemonCommandCallsSnapshot() -> [[String]] {
|
||||
self.testingDaemonCommandCalls
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
30
apps/macos/Sources/OpenClaw/GatewayNativeHostPolicy.swift
Normal file
30
apps/macos/Sources/OpenClaw/GatewayNativeHostPolicy.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
|
||||
enum GatewayNativeHostPolicy {
|
||||
static let environmentKey = "OPENCLAW_MAC_NATIVE_GATEWAY"
|
||||
|
||||
private static let enabledValues: Set<String> = ["1", "true", "yes", "on", "native", "app"]
|
||||
private static let disabledValues: Set<String> = ["0", "false", "no", "off", "launchd"]
|
||||
|
||||
static func shouldPreferNativeHost(
|
||||
mode: AppState.ConnectionMode,
|
||||
defaults: UserDefaults = .standard,
|
||||
environment: [String: String] = ProcessInfo.processInfo.environment) -> Bool
|
||||
{
|
||||
guard mode == .local else { return false }
|
||||
if let envValue = environment[self.environmentKey].map(self.normalizeFlagValue),
|
||||
!envValue.isEmpty
|
||||
{
|
||||
if self.disabledValues.contains(envValue) { return false }
|
||||
if self.enabledValues.contains(envValue) { return true }
|
||||
}
|
||||
if defaults.object(forKey: gatewayNativeHostEnabledKey) != nil {
|
||||
return defaults.bool(forKey: gatewayNativeHostEnabledKey)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private static func normalizeFlagValue(_ value: String) -> String {
|
||||
value.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,8 @@ final class GatewayProcessManager {
|
||||
private var environmentRefreshTask: Task<Void, Never>?
|
||||
private var lastEnvironmentRefresh: Date?
|
||||
private var logRefreshTask: Task<Void, Never>?
|
||||
private var nativeGatewayProcess: Process?
|
||||
private var nativeGatewayOutputPipes: [Pipe] = []
|
||||
#if DEBUG
|
||||
private var testingConnection: GatewayConnection?
|
||||
private var testingSkipControlChannelRefresh = false
|
||||
@@ -80,6 +82,11 @@ final class GatewayProcessManager {
|
||||
|
||||
func ensureLaunchAgentEnabledIfNeeded() async {
|
||||
guard !CommandResolver.connectionModeIsRemote() else { return }
|
||||
if self.prefersNativeHostedGateway() {
|
||||
self.appendLog("[gateway] native host active; launchd auto-enable skipped\n")
|
||||
self.logger.info("gateway launchd auto-enable skipped (native host active)")
|
||||
return
|
||||
}
|
||||
if GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() {
|
||||
self.appendLog("[gateway] launchd auto-enable skipped (attach-only)\n")
|
||||
self.logger.info("gateway launchd auto-enable skipped (disable marker set)")
|
||||
@@ -117,6 +124,14 @@ final class GatewayProcessManager {
|
||||
// First try to latch onto an already-running gateway to avoid spawning a duplicate.
|
||||
Task { [weak self] in
|
||||
guard let self else { return }
|
||||
if self.prefersNativeHostedGateway() {
|
||||
let stoppedLaunchd = await self.stopLaunchdGatewayForNativeHostIfNeeded()
|
||||
if !stoppedLaunchd, await self.attachExistingGatewayIfAvailable() {
|
||||
return
|
||||
}
|
||||
await self.startNativeGateway()
|
||||
return
|
||||
}
|
||||
if await self.attachExistingGatewayIfAvailable() {
|
||||
return
|
||||
}
|
||||
@@ -130,6 +145,7 @@ final class GatewayProcessManager {
|
||||
self.lastFailureReason = nil
|
||||
self.status = .stopped
|
||||
self.logger.info("gateway stop requested")
|
||||
self.stopNativeGateway()
|
||||
if CommandResolver.connectionModeIsRemote() {
|
||||
return
|
||||
}
|
||||
@@ -171,6 +187,7 @@ final class GatewayProcessManager {
|
||||
|
||||
func refreshLog() {
|
||||
guard self.logRefreshTask == nil else { return }
|
||||
guard self.nativeGatewayProcess == nil else { return }
|
||||
let path = GatewayLaunchAgentManager.launchdGatewayLogPath()
|
||||
let limit = self.logLimit
|
||||
self.logRefreshTask = Task { [weak self] in
|
||||
@@ -357,6 +374,154 @@ final class GatewayProcessManager {
|
||||
self.logger.warning("gateway start timed out")
|
||||
}
|
||||
|
||||
private func prefersNativeHostedGateway() -> Bool {
|
||||
GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: .local,
|
||||
defaults: .standard,
|
||||
environment: ProcessInfo.processInfo.environment)
|
||||
}
|
||||
|
||||
private func stopLaunchdGatewayForNativeHostIfNeeded() async -> Bool {
|
||||
guard !GatewayLaunchAgentManager.isLaunchAgentWriteDisabled() else {
|
||||
self.appendLog(
|
||||
"[gateway] launchd stop skipped (attach-only); " +
|
||||
"native host will attach if a listener is present\n")
|
||||
return false
|
||||
}
|
||||
guard await GatewayLaunchAgentManager.isLoaded() else { return false }
|
||||
let bundlePath = Bundle.main.bundleURL.path
|
||||
self.appendLog("[gateway] disabling launchd job before native host start\n")
|
||||
let err = await GatewayLaunchAgentManager.set(
|
||||
enabled: false,
|
||||
bundlePath: bundlePath,
|
||||
port: GatewayEnvironment.gatewayPort())
|
||||
if let err {
|
||||
self.appendLog("[gateway] launchd disable before native host failed: \(err)\n")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func startNativeGateway() async {
|
||||
self.existingGatewayDetails = nil
|
||||
let resolution = await Task.detached(priority: .utility) {
|
||||
GatewayEnvironment.resolveGatewayCommand()
|
||||
}.value
|
||||
self.environmentStatus = resolution.status
|
||||
guard let command = resolution.command, let executable = command.first else {
|
||||
self.status = .failed(resolution.status.message)
|
||||
self.lastFailureReason = resolution.status.message
|
||||
self.logger.error("native gateway command resolve failed: \(resolution.status.message)")
|
||||
return
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.executableURL = URL(fileURLWithPath: executable)
|
||||
process.arguments = Array(command.dropFirst())
|
||||
process.environment = self.nativeGatewayEnvironment()
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
process.standardOutput = outputPipe
|
||||
process.standardError = errorPipe
|
||||
self.installNativeGatewayOutputHandler(pipe: outputPipe, label: "stdout")
|
||||
self.installNativeGatewayOutputHandler(pipe: errorPipe, label: "stderr")
|
||||
|
||||
process.terminationHandler = { [weak self] terminated in
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self, self.nativeGatewayProcess === terminated else { return }
|
||||
self.nativeGatewayProcess = nil
|
||||
for pipe in self.nativeGatewayOutputPipes {
|
||||
pipe.fileHandleForReading.readabilityHandler = nil
|
||||
}
|
||||
self.nativeGatewayOutputPipes.removeAll()
|
||||
let status = terminated.terminationStatus
|
||||
self.appendLog("[gateway] native-hosted gateway exited with status \(status)\n")
|
||||
if self.desiredActive {
|
||||
let message = "Native-hosted Gateway exited with status \(status)"
|
||||
self.status = .failed(message)
|
||||
self.lastFailureReason = message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
let message = "Native-hosted Gateway failed to start: \(error.localizedDescription)"
|
||||
self.status = .failed(message)
|
||||
self.lastFailureReason = message
|
||||
self.appendLog("[gateway] \(message)\n")
|
||||
self.logger.error("\(message, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
self.nativeGatewayProcess = process
|
||||
self.nativeGatewayOutputPipes = [outputPipe, errorPipe]
|
||||
let port = GatewayEnvironment.gatewayPort()
|
||||
self.appendLog("[gateway] started native-hosted gateway pid \(process.processIdentifier) on port \(port)\n")
|
||||
self.logger.info("native-hosted gateway started pid=\(process.processIdentifier)")
|
||||
|
||||
let deadline = Date().addingTimeInterval(6)
|
||||
while Date() < deadline {
|
||||
if !self.desiredActive { return }
|
||||
if !process.isRunning {
|
||||
let message = "Native-hosted Gateway exited before readiness"
|
||||
self.status = .failed(message)
|
||||
self.lastFailureReason = message
|
||||
return
|
||||
}
|
||||
do {
|
||||
_ = try await self.connection.requestRaw(method: .health, timeoutMs: 1500)
|
||||
let details = "native host, pid \(process.processIdentifier)"
|
||||
self.clearLastFailure()
|
||||
self.status = .running(details: details)
|
||||
self.logger.info("native-hosted gateway ready details=\(details)")
|
||||
self.refreshControlChannelIfNeeded(reason: "native gateway started")
|
||||
return
|
||||
} catch {
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
}
|
||||
}
|
||||
|
||||
self.status = .failed("Native-hosted Gateway did not start in time")
|
||||
self.lastFailureReason = "native gateway start timeout"
|
||||
self.stopNativeGateway()
|
||||
self.logger.warning("native-hosted gateway start timed out")
|
||||
}
|
||||
|
||||
private func nativeGatewayEnvironment() -> [String: String] {
|
||||
var env = ProcessInfo.processInfo.environment
|
||||
env["PATH"] = CommandResolver.preferredPaths().joined(separator: ":")
|
||||
env["OPENCLAW_MAC_NATIVE_HOST"] = "1"
|
||||
env["OPENCLAW_MAC_NATIVE_HOST_BUNDLE_ID"] = Bundle.main.bundleIdentifier ?? launchdLabel
|
||||
env["OPENCLAW_GATEWAY_SUPERVISOR"] = "openclaw-macos-app"
|
||||
return env
|
||||
}
|
||||
|
||||
private func installNativeGatewayOutputHandler(pipe: Pipe, label: String) {
|
||||
pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in
|
||||
let data = handle.availableData
|
||||
guard !data.isEmpty else { return }
|
||||
let text = String(data: data, encoding: .utf8) ?? "<\(data.count) bytes>\n"
|
||||
Task { @MainActor [weak self] in
|
||||
self?.appendLog("[gateway \(label)] \(text)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func stopNativeGateway() {
|
||||
let process = self.nativeGatewayProcess
|
||||
self.nativeGatewayProcess = nil
|
||||
for pipe in self.nativeGatewayOutputPipes {
|
||||
pipe.fileHandleForReading.readabilityHandler = nil
|
||||
}
|
||||
self.nativeGatewayOutputPipes.removeAll()
|
||||
guard let process, process.isRunning else { return }
|
||||
self.appendLog("[gateway] stopping native-hosted gateway pid \(process.processIdentifier)\n")
|
||||
process.terminate()
|
||||
}
|
||||
|
||||
private func appendLog(_ chunk: String) {
|
||||
self.log.append(chunk)
|
||||
if self.log.count > self.logLimit {
|
||||
|
||||
@@ -98,10 +98,16 @@ struct OpenClawApp: App {
|
||||
private static func applyAttachOnlyOverrideIfNeeded() {
|
||||
let args = CommandLine.arguments
|
||||
guard args.contains("--attach-only") || args.contains("--no-launchd") else { return }
|
||||
if let error = GatewayLaunchAgentManager.applyAttachOnlyRuntimeOverride() {
|
||||
if let error = GatewayLaunchAgentManager.setLaunchAgentWriteDisabled(true) {
|
||||
Self.logger.error("attach-only flag failed: \(error, privacy: .public)")
|
||||
return
|
||||
}
|
||||
Task {
|
||||
_ = await GatewayLaunchAgentManager.set(
|
||||
enabled: false,
|
||||
bundlePath: Bundle.main.bundlePath,
|
||||
port: GatewayEnvironment.gatewayPort())
|
||||
}
|
||||
Self.logger.info("attach-only flag enabled")
|
||||
}
|
||||
|
||||
|
||||
@@ -1851,11 +1851,11 @@ public struct SessionsMessagesUnsubscribeParams: Codable, Sendable {
|
||||
}
|
||||
|
||||
public struct SessionsAbortParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let key: String
|
||||
public let runid: String?
|
||||
|
||||
public init(
|
||||
key: String?,
|
||||
key: String,
|
||||
runid: String?)
|
||||
{
|
||||
self.key = key
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@@ -10,10 +11,11 @@ struct GatewayAutostartPolicyTests {
|
||||
#expect(!GatewayAutostartPolicy.shouldStartGateway(mode: .unconfigured, paused: false))
|
||||
}
|
||||
|
||||
@Test func `ensures launch agent when local and not attach only`() {
|
||||
#expect(GatewayAutostartPolicy.shouldEnsureLaunchAgent(
|
||||
@Test func `skips launch agent when native host is preferred`() {
|
||||
#expect(!GatewayAutostartPolicy.shouldEnsureLaunchAgent(
|
||||
mode: .local,
|
||||
paused: false))
|
||||
paused: false,
|
||||
defaults: Self.cleanDefaults()))
|
||||
#expect(!GatewayAutostartPolicy.shouldEnsureLaunchAgent(
|
||||
mode: .local,
|
||||
paused: true))
|
||||
@@ -21,4 +23,61 @@ struct GatewayAutostartPolicyTests {
|
||||
mode: .remote,
|
||||
paused: false))
|
||||
}
|
||||
|
||||
@Test func `launch agent remains fallback when native host disabled`() {
|
||||
#expect(GatewayAutostartPolicy.shouldEnsureLaunchAgent(
|
||||
mode: .local,
|
||||
paused: false,
|
||||
environment: [GatewayNativeHostPolicy.environmentKey: "launchd"]))
|
||||
#expect(!GatewayAutostartPolicy.shouldEnsureLaunchAgent(
|
||||
mode: .local,
|
||||
paused: false,
|
||||
environment: [GatewayNativeHostPolicy.environmentKey: "native"]))
|
||||
}
|
||||
}
|
||||
|
||||
@Suite(.serialized)
|
||||
struct GatewayNativeHostPolicyTests {
|
||||
@Test func `prefers native host for local mode by default`() {
|
||||
let defaults = Self.cleanDefaults()
|
||||
#expect(GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: .local,
|
||||
defaults: defaults,
|
||||
environment: [:]))
|
||||
#expect(!GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: .remote,
|
||||
defaults: defaults,
|
||||
environment: [:]))
|
||||
#expect(!GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: .unconfigured,
|
||||
defaults: defaults,
|
||||
environment: [:]))
|
||||
}
|
||||
|
||||
@Test func `environment can force launchd fallback or native host`() {
|
||||
#expect(!GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: .local,
|
||||
environment: [GatewayNativeHostPolicy.environmentKey: "0"]))
|
||||
#expect(!GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: .local,
|
||||
environment: [GatewayNativeHostPolicy.environmentKey: "launchd"]))
|
||||
#expect(GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: .local,
|
||||
environment: [GatewayNativeHostPolicy.environmentKey: "1"]))
|
||||
#expect(GatewayNativeHostPolicy.shouldPreferNativeHost(
|
||||
mode: .local,
|
||||
environment: [GatewayNativeHostPolicy.environmentKey: "native"]))
|
||||
}
|
||||
}
|
||||
|
||||
private extension GatewayAutostartPolicyTests {
|
||||
static func cleanDefaults() -> UserDefaults {
|
||||
UserDefaults(suiteName: "GatewayAutostartPolicyTests.\(UUID().uuidString)")!
|
||||
}
|
||||
}
|
||||
|
||||
private extension GatewayNativeHostPolicyTests {
|
||||
static func cleanDefaults() -> UserDefaults {
|
||||
UserDefaults(suiteName: "GatewayNativeHostPolicyTests.\(UUID().uuidString)")!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,29 +3,6 @@ import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
struct GatewayLaunchAgentManagerTests {
|
||||
@Test func `attach only runtime override does not uninstall gateway launch agent`() throws {
|
||||
let dir = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-attach-only-\(UUID().uuidString)", isDirectory: true)
|
||||
let marker = dir.appendingPathComponent("disable-launchagent")
|
||||
try FileManager().createDirectory(at: dir, withIntermediateDirectories: true)
|
||||
defer { try? FileManager().removeItem(at: dir) }
|
||||
defer {
|
||||
GatewayLaunchAgentManager.setTestingDisableLaunchAgentMarkerURL(nil)
|
||||
GatewayLaunchAgentManager.setTestingInterceptDaemonCommands(false)
|
||||
GatewayLaunchAgentManager.clearTestingDaemonCommandCalls()
|
||||
}
|
||||
|
||||
GatewayLaunchAgentManager.setTestingDisableLaunchAgentMarkerURL(marker)
|
||||
GatewayLaunchAgentManager.setTestingInterceptDaemonCommands(true)
|
||||
GatewayLaunchAgentManager.clearTestingDaemonCommandCalls()
|
||||
|
||||
let error = GatewayLaunchAgentManager.applyAttachOnlyRuntimeOverride()
|
||||
|
||||
#expect(error == nil)
|
||||
#expect(FileManager().fileExists(atPath: marker.path))
|
||||
#expect(GatewayLaunchAgentManager.testingDaemonCommandCallsSnapshot().isEmpty)
|
||||
}
|
||||
|
||||
@Test func `launch agent plist snapshot parses args and env`() throws {
|
||||
let url = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-launchd-\(UUID().uuidString).plist")
|
||||
|
||||
@@ -1851,11 +1851,11 @@ public struct SessionsMessagesUnsubscribeParams: Codable, Sendable {
|
||||
}
|
||||
|
||||
public struct SessionsAbortParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let key: String
|
||||
public let runid: String?
|
||||
|
||||
public init(
|
||||
key: String?,
|
||||
key: String,
|
||||
runid: String?)
|
||||
{
|
||||
self.key = key
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
c3bcb3a3da46bbbe15a7798869911cab109df950ee51c79fd86c96bb809dfdf1 config-baseline.json
|
||||
8f573caa7f4cf01ae9d4805d3d14e1ba6772f651f6da182baaf2b469592749a4 config-baseline.core.json
|
||||
92712871defa92eeda8161b516db85574681f2b70678b940508a808b987aeae2 config-baseline.channel.json
|
||||
aca3215b7382af82b5060d73c631a7f82661c6e99193fa5eb1c5b4b499fb657b config-baseline.plugin.json
|
||||
bfd8b3ddcac047e486e9c43fdedc002cb9bf87b659f6563f9f11c850c5b2aaef config-baseline.json
|
||||
8d75df355b7f6e44b9c2f195d9df86130beb697e26061469df7d60b7e8a2f204 config-baseline.core.json
|
||||
9f5fad66a49fa618d64a963470aa69fed9fe4b4639cc4321f9ec04bfb2f8aa50 config-baseline.channel.json
|
||||
c4231c2194206547af8ad94342dc00aadb734f43cb49cc79d4c46bdbb80c3f95 config-baseline.plugin.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
dd840b7c222ca003aa5336aabff8a126e3e254474941ddab93165e0e44944ffa plugin-sdk-api-baseline.json
|
||||
443878722940029e4ae5220f3c23ffc321559b73848f6a7a3f4cab98c076924e plugin-sdk-api-baseline.jsonl
|
||||
6e8aa3634daa81d054c339d2a8b6a526ec22b93e737980d21191ff7d53449eda plugin-sdk-api-baseline.json
|
||||
6bb635a9d95b671c24251406d098ac052a6773551a1db30529bdc97caf1bb735 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -27,15 +27,7 @@ Generated locale trees and live translation memory now live in the publish repo:
|
||||
|
||||
- Keep generated locale output out of the main product repo.
|
||||
- Keep Mintlify on a single published docs tree.
|
||||
- Preserve the built-in language switcher for Mintlify-supported generated locales by letting the publish repo own generated locale trees.
|
||||
- Keep generated Thai (`th`) and Persian (`fa`) docs plus translation memory even though Mintlify does not currently accept those codes in `navigation.languages`. Their absence from the built-in docs language picker is a host limitation, not a failed translation run.
|
||||
|
||||
## Locale visibility
|
||||
|
||||
- Control UI supports `en`, `zh-CN`, `zh-TW`, `pt-BR`, `de`, `es`, `ja-JP`, `ko`, `fr`, `ar`, `it`, `tr`, `uk`, `id`, `pl`, `th`, `vi`, `nl`, and `fa`.
|
||||
- Docs translation workflows generate the same non-English locale set in `openclaw/docs`.
|
||||
- The Mintlify docs language picker can expose only the locales accepted by Mintlify `navigation.languages`; today that includes Vietnamese (`vi`) and Dutch (`nl`), but not Thai (`th`) or Persian (`fa`).
|
||||
- Do not treat missing `th` or `fa` entries in generated `docs/docs.json` as a pipeline failure. Verify their generated folders in `openclaw/docs` instead.
|
||||
- Preserve the built-in language switcher by letting the publish repo own generated locale trees.
|
||||
|
||||
## Files in this folder
|
||||
|
||||
|
||||
@@ -11,14 +11,6 @@
|
||||
"source": "OpenAI provider",
|
||||
"target": "OpenAI provider"
|
||||
},
|
||||
{
|
||||
"source": "OpenClaw App SDK",
|
||||
"target": "OpenClaw 应用 SDK"
|
||||
},
|
||||
{
|
||||
"source": "OpenClaw App SDK API design",
|
||||
"target": "OpenClaw 应用 SDK API 设计"
|
||||
},
|
||||
{
|
||||
"source": "Azure Speech",
|
||||
"target": "Azure Speech"
|
||||
@@ -35,14 +27,6 @@
|
||||
"source": "Gateway",
|
||||
"target": "Gateway 网关"
|
||||
},
|
||||
{
|
||||
"source": "Gateway RPC reference",
|
||||
"target": "Gateway RPC 参考"
|
||||
},
|
||||
{
|
||||
"source": "Sessions",
|
||||
"target": "会话"
|
||||
},
|
||||
{
|
||||
"source": "Pi",
|
||||
"target": "Pi"
|
||||
@@ -67,10 +51,6 @@
|
||||
"source": "Agent loop",
|
||||
"target": "Agent loop"
|
||||
},
|
||||
{
|
||||
"source": "Steering queue",
|
||||
"target": "Steering queue"
|
||||
},
|
||||
{
|
||||
"source": "Models",
|
||||
"target": "Models"
|
||||
|
||||
@@ -115,7 +115,6 @@ openclaw gateway
|
||||
|
||||
If OpenClaw is already running as a background service, restart it via the OpenClaw Mac app or by stopping and restarting the `openclaw gateway run` process.
|
||||
For managed service installs, run `openclaw gateway install` from a shell where `DISCORD_BOT_TOKEN` is present, or store the variable in `~/.openclaw/.env`, so the service can resolve the env SecretRef after restart.
|
||||
If your host is blocked or rate-limited by Discord's startup application lookup, set the Discord application/client ID from the Developer Portal so startup can skip that REST call. Use `channels.discord.applicationId` for the default account, or `channels.discord.accounts.<accountId>.applicationId` when you run multiple Discord bots.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -153,28 +152,6 @@ DISCORD_BOT_TOKEN=...
|
||||
|
||||
For scripted or remote setup, write the same JSON5 block with `openclaw config patch --file ./discord.patch.json5 --dry-run` and then rerun without `--dry-run`. Plaintext `token` values are supported. SecretRef values are also supported for `channels.discord.token` across env/file/exec providers. See [Secrets Management](/gateway/secrets).
|
||||
|
||||
For multiple Discord bots, keep each bot token and application ID under its account. A top-level `channels.discord.applicationId` is inherited by accounts, so only set it there when every account should use the same application ID.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
discord: {
|
||||
enabled: true,
|
||||
accounts: {
|
||||
personal: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_PERSONAL_TOKEN" },
|
||||
applicationId: "111111111111111111",
|
||||
},
|
||||
work: {
|
||||
token: { source: "env", provider: "default", id: "DISCORD_WORK_TOKEN" },
|
||||
applicationId: "222222222222222222",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ Example:
|
||||
**Teams + channel allowlist**
|
||||
|
||||
- Scope group/channel replies by listing teams and channels under `channels.msteams.teams`.
|
||||
- Keys should use stable Teams conversation IDs from Teams links, not mutable display names.
|
||||
- Keys should use stable team IDs and channel conversation IDs.
|
||||
- When `groupPolicy="allowlist"` and a teams allowlist is present, only listed teams/channels are accepted (mention‑gated).
|
||||
- The configure wizard accepts `Team/Channel` entries and stores them for you.
|
||||
- On startup, OpenClaw resolves team/channel and user allowlist names to IDs (when Graph permissions allow)
|
||||
@@ -944,7 +944,7 @@ The `groupId` query parameter in Teams URLs is **NOT** the team ID used for conf
|
||||
```
|
||||
https://teams.microsoft.com/l/team/19%3ABk4j...%40thread.tacv2/conversations?groupId=...
|
||||
└────────────────────────────┘
|
||||
Team conversation ID (URL-decode this)
|
||||
Team ID (URL-decode this)
|
||||
```
|
||||
|
||||
**Channel URL:**
|
||||
@@ -957,9 +957,9 @@ https://teams.microsoft.com/l/channel/19%3A15bc...%40thread.tacv2/ChannelName?gr
|
||||
|
||||
**For config:**
|
||||
|
||||
- Team key = path segment after `/team/` (URL-decoded, e.g., `19:Bk4j...@thread.tacv2`; older tenants may show `@thread.skype`, which is also valid)
|
||||
- Channel key = path segment after `/channel/` (URL-decoded)
|
||||
- **Ignore** the `groupId` query parameter for OpenClaw routing. It is the Microsoft Entra group ID, not the Bot Framework conversation ID used in incoming Teams activities.
|
||||
- Team ID = path segment after `/team/` (URL-decoded, e.g., `19:Bk4j...@thread.tacv2`)
|
||||
- Channel ID = path segment after `/channel/` (URL-decoded)
|
||||
- **Ignore** the `groupId` query parameter
|
||||
|
||||
## Private channels
|
||||
|
||||
|
||||
@@ -398,6 +398,22 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Error visibility
|
||||
|
||||
`channels.whatsapp.exposeErrorText` controls whether agent/provider error text is delivered back into WhatsApp. The default is `true`. Set it to `false` to keep failures quiet on WhatsApp while preserving other channel behavior.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
exposeErrorText: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Per-account overrides use `channels.whatsapp.accounts.<id>.exposeErrorText`.
|
||||
|
||||
## Reply quoting
|
||||
|
||||
WhatsApp supports native reply quoting, where outbound replies visibly quote the inbound message. Control it with `channels.whatsapp.replyToMode`.
|
||||
@@ -569,15 +585,6 @@ Behavior notes:
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Reply appears in transcript but not in WhatsApp">
|
||||
Transcript rows record what the agent generated. WhatsApp delivery is checked separately: OpenClaw only treats an auto-reply as sent after Baileys returns an outbound message id for at least one visible text or media send.
|
||||
|
||||
Ack reactions are independent pre-reply receipts. A successful reaction does not prove that the later text or media reply was accepted by WhatsApp.
|
||||
|
||||
Check gateway logs for `auto-reply delivery failed` or `auto-reply was not accepted by WhatsApp provider`.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Group messages unexpectedly ignored">
|
||||
Check in this order:
|
||||
|
||||
@@ -674,7 +681,7 @@ Primary reference:
|
||||
High-signal WhatsApp fields:
|
||||
|
||||
- access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`
|
||||
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`
|
||||
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`, `exposeErrorText`
|
||||
- multi-account: `accounts.<id>.enabled`, `accounts.<id>.authDir`, account-level overrides
|
||||
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*`
|
||||
- session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit`
|
||||
|
||||
739
docs/ci.md
739
docs/ci.md
File diff suppressed because one or more lines are too long
@@ -265,7 +265,7 @@ openclaw plugins deps --prune
|
||||
openclaw plugins deps --json
|
||||
```
|
||||
|
||||
`plugins deps` inspects the packaged runtime dependency stage for OpenClaw-owned bundled plugins selected by plugin config, enabled/configured channels, configured model providers, or bundled manifest defaults. It is not the install/update path for third-party npm or ClawHub plugins.
|
||||
`plugins deps` inspects the packaged runtime dependency stage for OpenClaw-owned bundled plugins. It is not the install/update path for third-party npm or ClawHub plugins.
|
||||
|
||||
Use `--repair` when a packaged install reports missing bundled runtime dependencies during Gateway startup or `plugins doctor`. Repair installs only missing enabled bundled-plugin deps with lifecycle scripts disabled. Use `--prune` to remove stale unknown external runtime-dependency roots left behind by older packaged layouts.
|
||||
|
||||
|
||||
@@ -86,15 +86,13 @@ Legacy session folders from other tools are not read.
|
||||
|
||||
When queue mode is `steer`, inbound messages are injected into the current run.
|
||||
Queued steering is delivered **after the current assistant turn finishes
|
||||
executing its tool calls**, before the next LLM call. Pi drains all pending
|
||||
steering messages together for `steer`; legacy `queue` drains one message per
|
||||
model boundary. Steering no longer skips remaining tool calls from the current
|
||||
assistant message.
|
||||
executing its tool calls**, before the next LLM call. Steering no longer skips
|
||||
remaining tool calls from the current assistant message; it injects the queued
|
||||
message at the next model boundary instead.
|
||||
|
||||
When queue mode is `followup` or `collect`, inbound messages are held until the
|
||||
current turn ends, then a new agent turn starts with the queued payloads. See
|
||||
[Queue](/concepts/queue) and [Steering queue](/concepts/queue-steering) for mode
|
||||
and boundary behavior.
|
||||
[Queue](/concepts/queue) for mode + debounce/cap behavior.
|
||||
|
||||
Block streaming sends completed assistant blocks as soon as they finish; it is
|
||||
**off by default** (`agents.defaults.blockStreamingDefault: "off"`).
|
||||
|
||||
@@ -127,9 +127,9 @@ current run, or collected for a followup turn.
|
||||
- Default mode is `steer`, with a 500ms followup debounce when steering falls
|
||||
back to queued followup delivery.
|
||||
- Modes: `steer`, `followup`, `collect`, `steer-backlog`, `interrupt`, and the
|
||||
legacy one-at-a-time `queue` mode.
|
||||
legacy `queue` alias.
|
||||
|
||||
Details: [Command queue](/concepts/queue) and [Steering queue](/concepts/queue-steering).
|
||||
Details: [Command queue](/concepts/queue).
|
||||
|
||||
## Channel run ownership
|
||||
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
---
|
||||
summary: "Public OpenClaw App SDK for external apps, scripts, dashboards, CI jobs, and IDE extensions"
|
||||
title: "OpenClaw App SDK"
|
||||
sidebarTitle: "App SDK"
|
||||
read_when:
|
||||
- You are building an external app, script, dashboard, CI job, or IDE extension that talks to OpenClaw
|
||||
- You are choosing between the App SDK and the Plugin SDK
|
||||
- You are integrating with Gateway agent runs, sessions, events, approvals, models, or tools
|
||||
---
|
||||
|
||||
The **OpenClaw App SDK** is the public client API for apps outside the
|
||||
OpenClaw process. Use `@openclaw/sdk` when a script, dashboard, CI job, IDE
|
||||
extension, or other external app wants to connect to the Gateway, start agent
|
||||
runs, stream events, wait for results, cancel work, or inspect Gateway
|
||||
resources.
|
||||
|
||||
<Note>
|
||||
The App SDK is different from the [Plugin SDK](/plugins/sdk-overview).
|
||||
`@openclaw/sdk` talks to the Gateway from outside OpenClaw.
|
||||
`openclaw/plugin-sdk/*` is only for plugins that run inside OpenClaw and
|
||||
register providers, channels, tools, hooks, or trusted runtimes.
|
||||
</Note>
|
||||
|
||||
## What Ships Today
|
||||
|
||||
`@openclaw/sdk` ships with:
|
||||
|
||||
| Surface | Status | What it does |
|
||||
| ------------------------- | ------- | ---------------------------------------------------------------------------- |
|
||||
| `OpenClaw` | Ready | Main client entry point. Owns transport, connection, requests, and events. |
|
||||
| `GatewayClientTransport` | Ready | WebSocket transport backed by the Gateway client. |
|
||||
| `oc.agents` | Ready | Lists, creates, updates, deletes, and gets agent handles. |
|
||||
| `Agent.run()` | Ready | Starts a Gateway `agent` run and returns a `Run`. |
|
||||
| `oc.runs` | Ready | Creates, gets, waits for, cancels, and streams runs. |
|
||||
| `Run.events()` | Ready | Streams normalized per-run events with replay for fast runs. |
|
||||
| `Run.wait()` | Ready | Calls `agent.wait` and returns a stable `RunResult`. |
|
||||
| `Run.cancel()` | Ready | Calls `sessions.abort` by run id, with session key when available. |
|
||||
| `oc.sessions` | Ready | Creates, resolves, sends to, patches, compacts, and gets session handles. |
|
||||
| `Session.send()` | Ready | Calls `sessions.send` and returns a `Run`. |
|
||||
| `oc.models` | Ready | Calls `models.list` and the current `models.authStatus` status RPC. |
|
||||
| `oc.tools` | Partial | Lists tool catalog and effective tools; direct tool invocation is not wired. |
|
||||
| `oc.approvals` | Ready | Lists and resolves exec approvals through Gateway approval RPCs. |
|
||||
| `oc.rawEvents()` | Ready | Exposes raw Gateway events for advanced consumers. |
|
||||
| `normalizeGatewayEvent()` | Ready | Converts raw Gateway events into the stable SDK event shape. |
|
||||
|
||||
The SDK also exports the core types used by those surfaces:
|
||||
`AgentRunParams`, `RunResult`, `RunStatus`, `OpenClawEvent`,
|
||||
`OpenClawEventType`, `GatewayEvent`, `OpenClawTransport`,
|
||||
`GatewayRequestOptions`, `SessionCreateParams`, `SessionSendParams`,
|
||||
`RuntimeSelection`, `EnvironmentSelection`, `WorkspaceSelection`,
|
||||
`ApprovalMode`, and related result types.
|
||||
|
||||
## Connect To A Gateway
|
||||
|
||||
Create a client with an explicit Gateway URL, or inject a custom transport for
|
||||
tests and embedded app runtimes.
|
||||
|
||||
```typescript
|
||||
import { OpenClaw } from "@openclaw/sdk";
|
||||
|
||||
const oc = new OpenClaw({
|
||||
url: "ws://127.0.0.1:14565",
|
||||
token: process.env.OPENCLAW_GATEWAY_TOKEN,
|
||||
requestTimeoutMs: 30_000,
|
||||
});
|
||||
|
||||
await oc.connect();
|
||||
```
|
||||
|
||||
`new OpenClaw({ gateway: "ws://..." })` is equivalent to `url`. The
|
||||
`gateway: "auto"` option is accepted by the constructor, but automatic Gateway
|
||||
discovery is not a separate SDK feature yet; pass `url` when the app does not
|
||||
already know how to discover the Gateway.
|
||||
|
||||
For tests, pass an object that implements `OpenClawTransport`:
|
||||
|
||||
```typescript
|
||||
const oc = new OpenClaw({
|
||||
transport: {
|
||||
async request(method, params) {
|
||||
return { method, params };
|
||||
},
|
||||
async *events() {},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Run An Agent
|
||||
|
||||
Use `oc.agents.get(id)` when the app wants an agent handle, then call
|
||||
`agent.run()`.
|
||||
|
||||
```typescript
|
||||
const agent = await oc.agents.get("main");
|
||||
|
||||
const run = await agent.run({
|
||||
input: "Review this pull request and suggest the smallest safe fix.",
|
||||
model: "openai/gpt-5.5",
|
||||
sessionKey: "main",
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
|
||||
for await (const event of run.events()) {
|
||||
const data = event.data as { delta?: unknown };
|
||||
if (event.type === "assistant.delta" && typeof data.delta === "string") {
|
||||
process.stdout.write(data.delta);
|
||||
}
|
||||
}
|
||||
|
||||
const result = await run.wait({ timeoutMs: 120_000 });
|
||||
console.log(result.status);
|
||||
```
|
||||
|
||||
Provider-qualified model refs such as `openai/gpt-5.5` are split into Gateway
|
||||
`provider` and `model` overrides. `timeoutMs` stays milliseconds in the SDK and
|
||||
is converted to Gateway timeout seconds for the `agent` RPC.
|
||||
|
||||
`run.wait()` uses the Gateway `agent.wait` RPC. A wait deadline that expires
|
||||
while the run is still active returns `status: "accepted"` instead of pretending
|
||||
the run itself timed out. Runtime timeouts, aborted runs, and cancelled runs are
|
||||
normalized into `timed_out` or `cancelled`.
|
||||
|
||||
## Create And Reuse Sessions
|
||||
|
||||
Use sessions when the app wants durable transcript state.
|
||||
|
||||
```typescript
|
||||
const session = await oc.sessions.create({
|
||||
agentId: "main",
|
||||
label: "release-review",
|
||||
});
|
||||
|
||||
const run = await session.send("Prepare release notes from the current diff.");
|
||||
await run.wait();
|
||||
```
|
||||
|
||||
`Session.send()` calls `sessions.send` and returns a `Run`. Session handles also
|
||||
support:
|
||||
|
||||
```typescript
|
||||
await session.abort(run.id);
|
||||
await session.patch({ label: "renamed-session" });
|
||||
await session.compact({ maxLines: 200 });
|
||||
```
|
||||
|
||||
## Stream Events
|
||||
|
||||
The SDK normalizes raw Gateway events into a stable `OpenClawEvent` envelope:
|
||||
|
||||
```typescript
|
||||
type OpenClawEvent = {
|
||||
version: 1;
|
||||
id: string;
|
||||
ts: number;
|
||||
type: OpenClawEventType;
|
||||
runId?: string;
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
taskId?: string;
|
||||
agentId?: string;
|
||||
data: unknown;
|
||||
raw?: GatewayEvent;
|
||||
};
|
||||
```
|
||||
|
||||
Common event types include:
|
||||
|
||||
| Event type | Source Gateway event |
|
||||
| --------------------- | ------------------------------------------- |
|
||||
| `run.started` | `agent` lifecycle start |
|
||||
| `run.completed` | `agent` lifecycle end |
|
||||
| `run.failed` | `agent` lifecycle error |
|
||||
| `run.cancelled` | Aborted/cancelled lifecycle end |
|
||||
| `run.timed_out` | Timeout lifecycle end |
|
||||
| `assistant.delta` | Assistant streaming delta |
|
||||
| `assistant.message` | Assistant message |
|
||||
| `thinking.delta` | Thinking or plan stream |
|
||||
| `tool.call.started` | Tool/item/command start |
|
||||
| `tool.call.delta` | Tool/item/command update |
|
||||
| `tool.call.completed` | Tool/item/command completion |
|
||||
| `tool.call.failed` | Tool/item/command failure or blocked status |
|
||||
| `approval.requested` | Exec or plugin approval request |
|
||||
| `approval.resolved` | Exec or plugin approval resolution |
|
||||
| `session.created` | `sessions.changed` create |
|
||||
| `session.updated` | `sessions.changed` update |
|
||||
| `session.compacted` | `sessions.changed` compaction |
|
||||
| `task.updated` | Task update events |
|
||||
| `artifact.updated` | Patch stream events |
|
||||
| `raw` | Any event without a stable SDK mapping yet |
|
||||
|
||||
`Run.events()` filters events to one run id and replays already-seen events for
|
||||
fast runs. That means the documented flow is safe:
|
||||
|
||||
```typescript
|
||||
const run = await agent.run("Summarize the latest session.");
|
||||
|
||||
for await (const event of run.events()) {
|
||||
if (event.type === "run.completed") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For app-wide streams, use `oc.events()`. For raw Gateway frames, use
|
||||
`oc.rawEvents()`.
|
||||
|
||||
## Models, Tools, And Approvals
|
||||
|
||||
Model helpers map to current Gateway methods:
|
||||
|
||||
```typescript
|
||||
await oc.models.list();
|
||||
await oc.models.status({ probe: false }); // calls models.authStatus
|
||||
```
|
||||
|
||||
Tool helpers expose the Gateway catalog and effective tool view:
|
||||
|
||||
```typescript
|
||||
await oc.tools.list();
|
||||
await oc.tools.effective({ sessionKey: "main" });
|
||||
```
|
||||
|
||||
Approval helpers use the exec approval RPCs:
|
||||
|
||||
```typescript
|
||||
const approvals = await oc.approvals.list();
|
||||
await oc.approvals.respond("approval-id", { decision: "approve" });
|
||||
```
|
||||
|
||||
## Explicitly Unsupported Today
|
||||
|
||||
The SDK includes names for the product model we want, but it does not silently
|
||||
pretend Gateway RPCs exist. These calls currently throw explicit unsupported
|
||||
errors:
|
||||
|
||||
```typescript
|
||||
await oc.tasks.list();
|
||||
await oc.tasks.get("task-id");
|
||||
await oc.tasks.cancel("task-id");
|
||||
|
||||
await oc.tools.invoke("tool-name", {});
|
||||
|
||||
await oc.artifacts.list();
|
||||
await oc.artifacts.get("artifact-id");
|
||||
await oc.artifacts.download("artifact-id");
|
||||
|
||||
await oc.environments.list();
|
||||
await oc.environments.create({});
|
||||
await oc.environments.status("environment-id");
|
||||
await oc.environments.delete("environment-id");
|
||||
```
|
||||
|
||||
Per-run `workspace`, `runtime`, `environment`, and `approvals` fields are typed
|
||||
as future shape, but the current Gateway does not support those overrides on
|
||||
the `agent` RPC. If callers pass them, the SDK throws before submitting the run
|
||||
so work does not accidentally execute with default workspace, runtime,
|
||||
environment, or approval behavior.
|
||||
|
||||
## App SDK Versus Plugin SDK
|
||||
|
||||
Use the App SDK when code lives outside OpenClaw:
|
||||
|
||||
- Node scripts that start or observe agent runs
|
||||
- CI jobs that call a Gateway
|
||||
- dashboards and admin panels
|
||||
- IDE extensions
|
||||
- external bridges that do not need to become channel plugins
|
||||
- integration tests with fake or real Gateway transports
|
||||
|
||||
Use the Plugin SDK when code runs inside OpenClaw:
|
||||
|
||||
- provider plugins
|
||||
- channel plugins
|
||||
- tool or lifecycle hooks
|
||||
- agent harness plugins
|
||||
- trusted runtime helpers
|
||||
|
||||
App SDK code should import from `@openclaw/sdk`. Plugin code should import from
|
||||
documented `openclaw/plugin-sdk/*` subpaths. Do not mix the two contracts.
|
||||
|
||||
## Related Docs
|
||||
|
||||
- [OpenClaw App SDK API design](/reference/openclaw-sdk-api-design)
|
||||
- [Gateway RPC reference](/reference/rpc)
|
||||
- [Agent loop](/concepts/agent-loop)
|
||||
- [Agent runtimes](/concepts/agent-runtimes)
|
||||
- [Sessions](/concepts/session)
|
||||
- [Background tasks](/automation/tasks)
|
||||
- [ACP agents](/tools/acp-agents)
|
||||
- [Plugin SDK overview](/plugins/sdk-overview)
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
summary: "How active-run steering queues messages at runtime boundaries"
|
||||
read_when:
|
||||
- Explaining how steer behaves while an agent is using tools
|
||||
- Changing active-run queue behavior or runtime steering integration
|
||||
- Comparing steer, queue, collect, and followup modes
|
||||
title: "Steering queue"
|
||||
---
|
||||
|
||||
When a message arrives while a session run is already streaming, OpenClaw can
|
||||
send that message into the active runtime instead of starting another run for
|
||||
the same session. The public modes are runtime-neutral; Pi and the native Codex
|
||||
app-server harness implement the delivery details differently.
|
||||
|
||||
## Runtime boundary
|
||||
|
||||
Steering does not interrupt a tool call that is already running. Pi checks for
|
||||
queued steering messages at model boundaries:
|
||||
|
||||
1. The assistant asks for tool calls.
|
||||
2. Pi executes the current assistant message's tool-call batch.
|
||||
3. Pi emits the turn end event.
|
||||
4. Pi drains queued steering messages.
|
||||
5. Pi appends those messages as user messages before the next LLM call.
|
||||
|
||||
This keeps tool results paired with the assistant message that requested them,
|
||||
then lets the next model call see the latest user input.
|
||||
|
||||
The native Codex app-server harness exposes `turn/steer` instead of Pi's
|
||||
internal steering queue. OpenClaw adapts the same modes there:
|
||||
|
||||
- `steer` batches queued messages for the configured quiet window, then sends a
|
||||
single `turn/steer` request with all collected user input in arrival order.
|
||||
- `queue` keeps the legacy serialized shape by sending separate `turn/steer`
|
||||
requests.
|
||||
- `followup`, `collect`, `steer-backlog`, and `interrupt` stay OpenClaw-owned
|
||||
queue behavior around the active Codex turn.
|
||||
|
||||
Codex review and manual compaction turns reject same-turn steering. When a
|
||||
runtime cannot accept steering, OpenClaw falls back to the followup queue where
|
||||
that mode allows it.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Active-run behavior | Later followup behavior |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `steer` | Injects all queued steering messages together at the next runtime boundary. This is the default. | Falls back to followup only when steering is unavailable. |
|
||||
| `queue` | Legacy one-at-a-time steering. Pi injects one queued message per model boundary; Codex sends separate `turn/steer` requests. | Falls back to followup only when steering is unavailable. |
|
||||
| `steer-backlog` | Same active-run steering behavior as `steer`. | Also keeps the same message for a later followup turn. |
|
||||
| `followup` | Does not steer the current run. | Runs queued messages later. |
|
||||
| `collect` | Does not steer the current run. | Coalesces compatible queued messages into one later turn after the debounce window. |
|
||||
| `interrupt` | Aborts the active run, then starts the newest message. | None. |
|
||||
|
||||
## Burst example
|
||||
|
||||
If four users send messages while the agent is executing a tool call:
|
||||
|
||||
- `steer`: the active runtime receives all four messages in arrival order before
|
||||
its next model decision. Pi drains them at the next model boundary; Codex
|
||||
receives them as one batched `turn/steer`.
|
||||
- `queue`: legacy serialized steering. Pi injects one queued message at a time;
|
||||
Codex receives separate `turn/steer` requests.
|
||||
- `collect`: OpenClaw waits until the active run ends, then creates a followup
|
||||
turn with compatible queued messages after the debounce window.
|
||||
|
||||
## Scope
|
||||
|
||||
Steering always targets the current active session run. It does not create a new
|
||||
session, change the active run's tool policy, or split messages by sender. In
|
||||
multi-user channels, inbound prompts already include sender and route context, so
|
||||
the next model call can see who sent each message.
|
||||
|
||||
Use `collect` when you want OpenClaw to build a later followup turn that can
|
||||
coalesce compatible messages and preserve followup queue drop policy. Use
|
||||
`queue` only when you need the older one-at-a-time steering behavior.
|
||||
|
||||
## Debounce
|
||||
|
||||
`messages.queue.debounceMs` applies to followup delivery, including `collect`,
|
||||
`followup`, `steer-backlog`, and `steer` fallback when active-run steering is not
|
||||
available. For Pi, active `steer` itself does not use the debounce timer because
|
||||
Pi naturally batches messages until the next model boundary. For the native
|
||||
Codex harness, OpenClaw uses the same debounce value as the quiet window before
|
||||
sending the batched `turn/steer`.
|
||||
|
||||
## Related
|
||||
|
||||
- [Command queue](/concepts/queue)
|
||||
- [Messages](/concepts/messages)
|
||||
- [Agent loop](/concepts/agent-loop)
|
||||
@@ -31,28 +31,24 @@ When unset, all inbound channel surfaces use:
|
||||
- `drop: "summarize"`
|
||||
|
||||
`steer` is the default because it keeps the active model turn responsive without
|
||||
starting a second session run. It drains all steering messages that arrived
|
||||
before the next model boundary. If the current run cannot accept steering,
|
||||
starting a second session run. If the current run cannot accept steering,
|
||||
OpenClaw falls back to a followup queue entry.
|
||||
|
||||
## Queue modes
|
||||
|
||||
Inbound messages can steer the current run, wait for a followup turn, or do both:
|
||||
|
||||
- `steer`: queue steering messages into the active runtime. Pi delivers all pending steering messages **after the current assistant turn finishes executing its tool calls**, before the next LLM call; Codex app-server receives one batched `turn/steer`. If the run is not actively streaming or steering is unavailable, OpenClaw falls back to a followup queue entry.
|
||||
- `queue` (legacy): old one-at-a-time steering. Pi delivers one queued steering message at each model boundary; Codex app-server receives separate `turn/steer` requests. Prefer `steer` unless you need the previous serialized behavior.
|
||||
- `steer`: queue a steering message into the active Pi run. Pi delivers it **after the current assistant turn finishes executing its tool calls**, before the next LLM call. If the run is not actively streaming or steering is unavailable, OpenClaw falls back to a followup queue entry.
|
||||
- `followup`: enqueue each message for a later agent turn after the current run ends.
|
||||
- `collect`: coalesce queued messages into a **single** followup turn after the quiet window. If messages target different channels/threads, they drain individually to preserve routing.
|
||||
- `steer-backlog` (aka `steer+backlog`): steer now **and** preserve the same message for a followup turn.
|
||||
- `interrupt` (legacy): abort the active run for that session, then run the newest message.
|
||||
- `queue` (legacy alias): same as `steer`.
|
||||
|
||||
Steer-backlog means you can get a followup response after the steered run, so
|
||||
streaming surfaces can look like duplicates. Prefer `collect`/`steer` if you want
|
||||
one response per inbound message.
|
||||
|
||||
For runtime-specific timing and dependency behavior, see
|
||||
[Steering queue](/concepts/queue-steering).
|
||||
|
||||
Configure globally or per channel via `messages.queue`:
|
||||
|
||||
```json5
|
||||
@@ -71,7 +67,7 @@ Configure globally or per channel via `messages.queue`:
|
||||
|
||||
## Queue options
|
||||
|
||||
Options apply to `followup`, `collect`, and `steer-backlog` (and to `steer` or legacy `queue` when steering falls back to followup):
|
||||
Options apply to `followup`, `collect`, and `steer-backlog` (and to `steer` when it falls back to followup):
|
||||
|
||||
- `debounceMs`: quiet window before draining queued followups. Bare numbers are milliseconds; units `ms`, `s`, `m`, `h`, and `d` are accepted by `/queue` options.
|
||||
- `cap`: max queued messages per session. Values below `1` are ignored.
|
||||
@@ -119,5 +115,4 @@ keys.
|
||||
## Related
|
||||
|
||||
- [Session management](/concepts/session)
|
||||
- [Steering queue](/concepts/queue-steering)
|
||||
- [Retry policy](/concepts/retry)
|
||||
|
||||
@@ -1163,8 +1163,7 @@
|
||||
"concepts/messages",
|
||||
"concepts/streaming",
|
||||
"concepts/retry",
|
||||
"concepts/queue",
|
||||
"concepts/queue-steering"
|
||||
"concepts/queue"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -1204,13 +1203,12 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Plugin SDK reference",
|
||||
"group": "SDK reference",
|
||||
"pages": [
|
||||
"plugins/sdk-overview",
|
||||
"plugins/sdk-subpaths",
|
||||
"plugins/sdk-entrypoints",
|
||||
"plugins/sdk-runtime",
|
||||
"plugins/sdk-channel-turn",
|
||||
"plugins/sdk-agent-harness",
|
||||
"plugins/sdk-setup",
|
||||
"plugins/sdk-testing",
|
||||
@@ -1653,12 +1651,7 @@
|
||||
},
|
||||
{
|
||||
"group": "RPC and API",
|
||||
"pages": [
|
||||
"reference/rpc",
|
||||
"concepts/openclaw-sdk",
|
||||
"reference/openclaw-sdk-api-design",
|
||||
"reference/device-models"
|
||||
]
|
||||
"pages": ["reference/rpc", "reference/device-models"]
|
||||
},
|
||||
{
|
||||
"group": "Templates",
|
||||
|
||||
@@ -1226,7 +1226,7 @@ See [Multi-Agent Sandbox & Tools](/tools/multi-agent-sandbox-tools) for preceden
|
||||
ackReactionScope: "group-mentions", // group-mentions | group-all | direct | all
|
||||
removeAckAfterReply: false,
|
||||
queue: {
|
||||
mode: "steer", // steer | queue (legacy one-at-a-time) | followup | collect | steer-backlog | steer+backlog | interrupt
|
||||
mode: "steer", // steer | followup | collect | steer-backlog | steer+backlog | queue | interrupt
|
||||
debounceMs: 500,
|
||||
cap: 20,
|
||||
drop: "summarize", // old | new | summarize
|
||||
|
||||
@@ -333,7 +333,7 @@ That stages grounded durable candidates into the short-term dreaming store while
|
||||
When sandboxing is enabled, doctor checks Docker images and offers to build or switch to legacy names if the current image is missing.
|
||||
</Accordion>
|
||||
<Accordion title="7b. Bundled plugin runtime deps">
|
||||
Doctor verifies runtime dependencies only for bundled plugins that are active in the current config or enabled by their bundled manifest default, for example `plugins.entries.discord.enabled: true`, legacy `channels.discord.enabled: true`, configured `models.providers.*` / agent model refs, or a default-enabled bundled plugin without provider ownership. If any are missing, doctor reports the packages and installs them in `openclaw doctor --fix` / `openclaw doctor --repair` mode. External plugins still use `openclaw plugins install` / `openclaw plugins update`; doctor does not install dependencies for arbitrary plugin paths.
|
||||
Doctor verifies runtime dependencies only for bundled plugins that are active in the current config or enabled by their bundled manifest default, for example `plugins.entries.discord.enabled: true`, legacy `channels.discord.enabled: true`, or a default-enabled bundled provider. If any are missing, doctor reports the packages and installs them in `openclaw doctor --fix` / `openclaw doctor --repair` mode. External plugins still use `openclaw plugins install` / `openclaw plugins update`; doctor does not install dependencies for arbitrary plugin paths.
|
||||
|
||||
During doctor repair, bundled runtime-dependency npm installs report spinner progress in TTY sessions and periodic line progress in piped/headless output. The Gateway and local CLI can also repair active bundled plugin runtime dependencies on demand before importing a bundled plugin. These installs are scoped to the plugin runtime install root, run with scripts disabled, do not write a package lock, and are guarded by an install-root lock so concurrent CLI or Gateway starts do not mutate the same `node_modules` tree at the same time.
|
||||
|
||||
|
||||
@@ -402,7 +402,7 @@ enumeration of `src/gateway/server-methods/*.ts`.
|
||||
- `sessions.create` creates a new session entry.
|
||||
- `sessions.send` sends a message into an existing session.
|
||||
- `sessions.steer` is the interrupt-and-steer variant for an active session.
|
||||
- `sessions.abort` aborts active work for a session. A caller may pass `key` plus optional `runId`, or pass `runId` alone for active runs the Gateway can resolve to a session.
|
||||
- `sessions.abort` aborts active work for a session.
|
||||
- `sessions.patch` updates session metadata/overrides and reports the resolved canonical model plus effective `agentRuntime`.
|
||||
- `sessions.reset`, `sessions.delete`, and `sessions.compact` perform session maintenance.
|
||||
- `sessions.get` returns the full stored session row.
|
||||
|
||||
@@ -1943,14 +1943,13 @@ lives on the [Models FAQ](/help/faq-models).
|
||||
<Accordion title='Why does it feel like the bot "ignores" rapid-fire messages?'>
|
||||
Queue mode controls how new messages interact with an in-flight run. Use `/queue` to change modes:
|
||||
|
||||
- `steer` - queue all pending steering for the next model boundary in the current run
|
||||
- `queue` - legacy one-at-a-time steering
|
||||
- `steer` - queue steering for the next model boundary in the current run
|
||||
- `followup` - run messages one at a time
|
||||
- `collect` - batch messages and reply once
|
||||
- `steer-backlog` - steer now, then process backlog
|
||||
- `interrupt` - abort current run and start fresh
|
||||
|
||||
Default mode is `steer`. You can add options like `debounce:0.5s cap:25 drop:summarize` for followup modes. See [Command queue](/concepts/queue) and [Steering queue](/concepts/queue-steering).
|
||||
Default mode is `steer`. You can add options like `debounce:0.5s cap:25 drop:summarize` for followup modes. See [Command queue](/concepts/queue).
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -202,12 +202,6 @@ Dangerous or privacy-heavy commands such as `camera.snap`, `camera.clip`, and
|
||||
`gateway.nodes.allowCommands`. `gateway.nodes.denyCommands` always wins over
|
||||
defaults and extra allowlist entries.
|
||||
|
||||
Plugin-owned node commands can add a Gateway node-invoke policy. That policy
|
||||
runs after the allowlist check and before forwarding to the node, so raw
|
||||
`node.invoke`, CLI helpers, and dedicated agent tools share the same plugin
|
||||
permission boundary. Dangerous plugin node commands still require explicit
|
||||
`gateway.nodes.allowCommands` opt-in.
|
||||
|
||||
After a node changes its declared command list, reject the old device pairing
|
||||
and approve the new request so the gateway stores the updated command snapshot.
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ read_when:
|
||||
title: "macOS app"
|
||||
---
|
||||
|
||||
The macOS app is the **menu‑bar companion** for OpenClaw. It owns permissions,
|
||||
manages/attaches to the Gateway locally (launchd or manual), and exposes macOS
|
||||
capabilities to the agent as a node.
|
||||
The macOS app is the **menu-bar companion** for OpenClaw. It owns permissions,
|
||||
manages/attaches to the Gateway locally (native host, launchd, or manual), and
|
||||
exposes macOS capabilities to the agent as a node.
|
||||
|
||||
## What it does
|
||||
|
||||
@@ -23,18 +23,47 @@ capabilities to the agent as a node.
|
||||
|
||||
## Local vs remote mode
|
||||
|
||||
- **Local** (default): the app attaches to a running local Gateway if present;
|
||||
otherwise it enables the launchd service via `openclaw gateway install`.
|
||||
- **Local** (default): the app hosts the Gateway as a child process under
|
||||
OpenClaw.app when no compatible Gateway is already running. This gives macOS
|
||||
TCC a native app identity for permission-sensitive desktop features such as
|
||||
Codex Computer Use.
|
||||
- **Remote**: the app connects to a Gateway over SSH/Tailscale and never starts
|
||||
a local process.
|
||||
The app starts the local **node host service** so the remote Gateway can reach this Mac.
|
||||
The app does not spawn the Gateway as a child process.
|
||||
The app does not spawn the Gateway as a child process in remote mode.
|
||||
Gateway discovery now prefers Tailscale MagicDNS names over raw tailnet IPs,
|
||||
so the Mac app recovers more reliably when tailnet IPs change.
|
||||
|
||||
Set `OPENCLAW_MAC_NATIVE_GATEWAY=launchd` before launching the app to use the
|
||||
legacy launchd fallback in local mode. Launchd is still useful for headless
|
||||
Gateway service management, but permission-sensitive Computer Use flows should
|
||||
run through the native-hosted Gateway.
|
||||
|
||||
## Native Gateway host
|
||||
|
||||
In native-host mode, OpenClaw.app starts the same Node Gateway command that the
|
||||
CLI would run, marks the child environment with `OPENCLAW_MAC_NATIVE_HOST=1`,
|
||||
and supervises its logs/readiness from the menu bar. Codex still runs inside the
|
||||
Gateway; OpenClaw.app is the macOS identity and lifecycle parent, not a Codex
|
||||
runtime replacement.
|
||||
|
||||
This shape keeps the runtime architecture stable:
|
||||
|
||||
```text
|
||||
OpenClaw.app native host
|
||||
-> OpenClaw Gateway
|
||||
-> Codex app-server
|
||||
-> Codex Computer Use MCP
|
||||
```
|
||||
|
||||
The app disables its managed launchd Gateway before starting a native-hosted
|
||||
Gateway so the local port does not stay occupied by a background Node lineage.
|
||||
If an unmanaged listener is already on the Gateway port, the app attaches to it
|
||||
and reports that existing instance instead of replacing it.
|
||||
|
||||
## Launchd control
|
||||
|
||||
The app manages a per‑user LaunchAgent labeled `ai.openclaw.gateway`
|
||||
The app can manage a per-user LaunchAgent labeled `ai.openclaw.gateway`
|
||||
(or `ai.openclaw.<profile>` when using `--profile`/`OPENCLAW_PROFILE`; legacy `com.openclaw.*` still unloads).
|
||||
|
||||
```bash
|
||||
@@ -44,8 +73,9 @@ launchctl bootout gui/$UID/ai.openclaw.gateway
|
||||
|
||||
Replace the label with `ai.openclaw.<profile>` when running a named profile.
|
||||
|
||||
If the LaunchAgent isn’t installed, enable it from the app or run
|
||||
`openclaw gateway install`.
|
||||
If the LaunchAgent is not installed, run `openclaw gateway install`. Local mode
|
||||
normally uses the native host first; launchd remains the fallback for headless
|
||||
service installs and explicit `OPENCLAW_MAC_NATIVE_GATEWAY=launchd` sessions.
|
||||
|
||||
## Node capabilities (mac)
|
||||
|
||||
|
||||
@@ -19,6 +19,27 @@ then lets Codex own the native MCP tool calls during Codex-mode turns.
|
||||
Use this page when OpenClaw is already using the native Codex harness. For the
|
||||
runtime setup itself, see [Codex harness](/plugins/codex-harness).
|
||||
|
||||
## macOS native host
|
||||
|
||||
On macOS, Codex Computer Use must run from a Gateway that OpenClaw.app launched
|
||||
in native-host mode. macOS grants Accessibility, Screen Recording, and
|
||||
Automation permissions to a responsible app identity, not to an abstract chat
|
||||
session. A launchd-started Gateway runs from a headless Node lineage, which can
|
||||
leave Codex Computer Use blocked by TCC even when the user already approved the
|
||||
prompts in the app.
|
||||
|
||||
The supported local shape is:
|
||||
|
||||
```text
|
||||
OpenClaw.app native host
|
||||
-> OpenClaw Gateway
|
||||
-> Codex app-server
|
||||
-> Codex Computer Use MCP
|
||||
```
|
||||
|
||||
Codex still lives inside the Gateway. OpenClaw.app only provides the macOS
|
||||
identity and lifecycle parent needed for desktop permissions.
|
||||
|
||||
## OpenClaw.app and Peekaboo
|
||||
|
||||
OpenClaw.app's Peekaboo integration is separate from Codex Computer Use. The
|
||||
@@ -115,6 +136,12 @@ register the bundled Codex marketplace from
|
||||
fails. If setup still cannot make the MCP server available, the turn fails
|
||||
before the thread starts.
|
||||
|
||||
On macOS, setup first verifies that the Gateway was launched by OpenClaw.app in
|
||||
native-host mode. If not, `/codex computer-use status` reports
|
||||
`native_host_missing` and does not contact Codex app-server. Open OpenClaw.app,
|
||||
switch to Local mode, wait for the Gateway to show running, then run
|
||||
`/codex computer-use install` again.
|
||||
|
||||
Existing sessions keep their runtime and Codex thread binding. After changing
|
||||
`agentRuntime` or Computer Use config, use `/new` or `/reset` in the affected
|
||||
chat before testing.
|
||||
@@ -229,6 +256,7 @@ status for chat:
|
||||
| Reason | Meaning | Next step |
|
||||
| ---------------------------- | ------------------------------------------------------ | --------------------------------------------- |
|
||||
| `disabled` | `computerUse.enabled` resolved to false. | Set `enabled` or another Computer Use field. |
|
||||
| `native_host_missing` | macOS Gateway is not hosted by OpenClaw.app. | Start Local mode from OpenClaw.app. |
|
||||
| `marketplace_missing` | No matching marketplace was available. | Configure source, path, or marketplace name. |
|
||||
| `plugin_not_installed` | Marketplace exists, but the plugin is not installed. | Run install or enable `autoInstall`. |
|
||||
| `plugin_disabled` | Plugin is installed but disabled in Codex config. | Run install to re-enable it. |
|
||||
@@ -244,9 +272,10 @@ when available, and the specific message for the failing setup step.
|
||||
## macOS permissions
|
||||
|
||||
Computer Use is macOS-specific. The Codex-owned MCP server may need local OS
|
||||
permissions before it can inspect or control apps. If OpenClaw says Computer Use
|
||||
is installed but the MCP server is unavailable, verify the Codex-side Computer
|
||||
Use setup first:
|
||||
permissions before it can inspect or control apps. If OpenClaw says the native
|
||||
host is missing, fix the Gateway host first. If OpenClaw says Computer Use is
|
||||
installed but the MCP server is unavailable, verify the Codex-side Computer Use
|
||||
setup next:
|
||||
|
||||
- Codex app-server is running on the same host where desktop control should
|
||||
happen.
|
||||
@@ -271,14 +300,19 @@ Codex app-server install writes the plugin config back to enabled.
|
||||
path. Remote-only catalog entries can be inspected but not installed through the
|
||||
current app-server API.
|
||||
|
||||
**Status says native host is required.** Launch OpenClaw.app, use Local mode,
|
||||
and let the app start the Gateway. The launchd fallback is fine for ordinary
|
||||
chat and service use, but Codex Computer Use on macOS needs the native app
|
||||
identity.
|
||||
|
||||
**Status says the MCP server is unavailable.** Re-run install once so MCP
|
||||
servers reload. If it remains unavailable, fix the Codex Computer Use app,
|
||||
Codex app-server MCP status, or macOS permissions.
|
||||
|
||||
**Status or a probe times out on `computer-use.list_apps`.** The plugin and MCP
|
||||
server are present, but the local Computer Use bridge did not answer. Quit or
|
||||
restart Codex Computer Use, relaunch Codex Desktop if needed, then retry in a
|
||||
fresh OpenClaw session.
|
||||
restart Codex Computer Use, confirm OpenClaw.app still owns the local Gateway,
|
||||
then retry in a fresh OpenClaw session.
|
||||
|
||||
**A Computer Use tool says `Native hook relay unavailable`.** The Codex-native
|
||||
tool hook could not reach an active OpenClaw relay through the local bridge or
|
||||
|
||||
@@ -946,14 +946,6 @@ originating chat, and the next queued follow-up message answers that native
|
||||
server request instead of being steered as extra context. Other MCP elicitation
|
||||
requests still fail closed.
|
||||
|
||||
Active-run queue steering maps onto Codex app-server `turn/steer`. With the
|
||||
default `messages.queue.mode: "steer"`, OpenClaw batches queued chat messages
|
||||
for the configured quiet window and sends them as one `turn/steer` request in
|
||||
arrival order. Legacy `queue` mode sends separate `turn/steer` requests. Codex
|
||||
review and manual compaction turns can reject same-turn steering, in which case
|
||||
OpenClaw uses the followup queue when the selected mode allows fallback. See
|
||||
[Steering queue](/concepts/queue-steering).
|
||||
|
||||
When the selected model uses the Codex harness, native thread compaction is
|
||||
delegated to Codex app-server. OpenClaw keeps a transcript mirror for channel
|
||||
history, search, `/new`, `/reset`, and future model or harness switching. The
|
||||
|
||||
@@ -166,10 +166,7 @@ health such as `inCall`, `manualActionRequired`, `providerConnected`,
|
||||
timestamps, byte counters, and bridge closed state. If a safe Meet page prompt
|
||||
appears, browser automation handles it when it can. Login, host admission, and
|
||||
browser/OS permission prompts are reported as manual action with a reason and
|
||||
message for the agent to relay. Managed Chrome sessions only emit the intro or
|
||||
test phrase after browser health reports `inCall: true`; otherwise status reports
|
||||
`speechReady: false` and the speech attempt is blocked instead of pretending the
|
||||
agent spoke into the meeting.
|
||||
message for the agent to relay.
|
||||
|
||||
Local Chrome joins through the signed-in OpenClaw browser profile. Realtime mode
|
||||
requires `BlackHole 2ch` for the microphone/speaker path used by OpenClaw. For
|
||||
@@ -1009,9 +1006,6 @@ a session ended.
|
||||
- `manualActionRequired` / `manualActionReason` / `manualActionMessage`: the
|
||||
browser profile needs manual login, Meet host admission, permissions, or
|
||||
browser-control repair before speech can work
|
||||
- `speechReady` / `speechBlockedReason` / `speechBlockedMessage`: whether
|
||||
managed Chrome speech is allowed now. `speechReady: false` means OpenClaw did
|
||||
not send the intro/test phrase into the audio bridge.
|
||||
- `providerConnected` / `realtimeReady`: realtime voice bridge state
|
||||
- `lastInputAt` / `lastOutputAt`: last audio seen from or sent to the bridge
|
||||
|
||||
|
||||
@@ -357,16 +357,7 @@ before runtime loads.
|
||||
{
|
||||
"id": "openai",
|
||||
"authMethods": ["api-key"],
|
||||
"envVars": ["OPENAI_API_KEY"],
|
||||
"authEvidence": [
|
||||
{
|
||||
"type": "local-file-with-env",
|
||||
"fileEnvVar": "OPENAI_CREDENTIALS_FILE",
|
||||
"requiresAllEnv": ["OPENAI_PROJECT"],
|
||||
"credentialMarker": "openai-local-credentials",
|
||||
"source": "openai local credentials"
|
||||
}
|
||||
]
|
||||
"envVars": ["OPENAI_API_KEY"]
|
||||
}
|
||||
],
|
||||
"cliBackends": ["openai-cli"],
|
||||
@@ -417,29 +408,11 @@ registration. These diagnostics are additive and do not reject legacy plugins.
|
||||
|
||||
### setup.providers reference
|
||||
|
||||
| Field | Required | Type | What it means |
|
||||
| -------------- | -------- | ---------- | ------------------------------------------------------------------------------------------------ |
|
||||
| `id` | Yes | `string` | Provider id exposed during setup or onboarding. Keep normalized ids globally unique. |
|
||||
| `authMethods` | No | `string[]` | Setup/auth method ids this provider supports without loading full runtime. |
|
||||
| `envVars` | No | `string[]` | Env vars that generic setup/status surfaces can check before plugin runtime loads. |
|
||||
| `authEvidence` | No | `object[]` | Cheap local auth evidence checks for providers that can authenticate through non-secret markers. |
|
||||
|
||||
`authEvidence` is for provider-owned local credential markers that can be
|
||||
verified without loading runtime code. These checks must stay cheap and local:
|
||||
no network calls, no keychain or secret-manager reads, no shell commands, and no
|
||||
provider API probes.
|
||||
|
||||
Supported evidence entries:
|
||||
|
||||
| Field | Required | Type | What it means |
|
||||
| ------------------ | -------- | ---------- | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | Yes | `string` | Currently `local-file-with-env`. |
|
||||
| `fileEnvVar` | No | `string` | Env var containing an explicit credential file path. |
|
||||
| `fallbackPaths` | No | `string[]` | Local credential file paths checked when `fileEnvVar` is absent or empty. Supports `${HOME}` and `${APPDATA}`. |
|
||||
| `requiresAnyEnv` | No | `string[]` | At least one listed env var must be non-empty before the evidence is valid. |
|
||||
| `requiresAllEnv` | No | `string[]` | Every listed env var must be non-empty before the evidence is valid. |
|
||||
| `credentialMarker` | Yes | `string` | Non-secret marker returned when the evidence is present. |
|
||||
| `source` | No | `string` | User-facing source label for auth/status output. |
|
||||
| Field | Required | Type | What it means |
|
||||
| ------------- | -------- | ---------- | ------------------------------------------------------------------------------------ |
|
||||
| `id` | Yes | `string` | Provider id exposed during setup or onboarding. Keep normalized ids globally unique. |
|
||||
| `authMethods` | No | `string[]` | Setup/auth method ids this provider supports without loading full runtime. |
|
||||
| `envVars` | No | `string[]` | Env vars that generic setup/status surfaces can check before plugin runtime loads. |
|
||||
|
||||
### setup fields
|
||||
|
||||
|
||||
@@ -681,9 +681,6 @@ Write colocated tests in `src/channel.test.ts`:
|
||||
<Card title="Runtime helpers" icon="settings" href="/plugins/sdk-runtime">
|
||||
TTS, STT, media, subagent via api.runtime
|
||||
</Card>
|
||||
<Card title="Channel turn kernel" icon="bolt" href="/plugins/sdk-channel-turn">
|
||||
Shared inbound turn lifecycle: ingest, resolve, record, dispatch, finalize
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Note>
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
---
|
||||
summary: "runtime.channel.turn -- the shared inbound turn kernel that bundled and third-party channel plugins use to record, dispatch, and finalize agent turns"
|
||||
title: "Channel turn kernel"
|
||||
sidebarTitle: "Channel turn"
|
||||
read_when:
|
||||
- You are building a channel plugin and want the shared inbound turn lifecycle
|
||||
- You are migrating a channel monitor off hand-rolled record/dispatch glue
|
||||
- You need to understand admission, ingest, classify, preflight, resolve, record, dispatch, and finalize stages
|
||||
---
|
||||
|
||||
The channel turn kernel is the shared inbound state machine that turns a normalized platform event into an agent turn. Channel plugins provide the platform facts and the delivery callback. Core owns the orchestration: ingest, classify, preflight, resolve, authorize, assemble, record, dispatch, and finalize.
|
||||
|
||||
Use this when your plugin is on the inbound message hot path. For non-message events (slash commands, modals, button interactions, lifecycle events, reactions, voice state), keep them plugin-local. The kernel only owns events that may become an agent text turn.
|
||||
|
||||
<Info>
|
||||
The kernel is reached through the injected plugin runtime as `runtime.channel.turn.*`. The plugin runtime type is exported from `openclaw/plugin-sdk/core`, so third-party native plugins can use these entry points the same way bundled channel plugins do.
|
||||
</Info>
|
||||
|
||||
## Why a shared kernel
|
||||
|
||||
Channel plugins repeat the same inbound flow: normalize, route, gate, build a context, record session metadata, dispatch the agent turn, finalize delivery state. Without a shared kernel, a change to mention gating, tool-only visible replies, session metadata, pending history, or dispatch finalization has to be applied per channel.
|
||||
|
||||
The kernel keeps four concepts deliberately separate:
|
||||
|
||||
- `ConversationFacts`: where the message came from
|
||||
- `RouteFacts`: which agent and session should process it
|
||||
- `ReplyPlanFacts`: where visible replies should go
|
||||
- `MessageFacts`: what body and supplemental context the agent should see
|
||||
|
||||
Slack DMs, Telegram topics, Matrix threads, and Feishu topic sessions all distinguish these in practice. Treating them as one identifier causes drift over time.
|
||||
|
||||
## Stage lifecycle
|
||||
|
||||
The kernel runs the same fixed pipeline regardless of channel:
|
||||
|
||||
1. `ingest` -- adapter converts a raw platform event into `NormalizedTurnInput`
|
||||
2. `classify` -- adapter declares whether this event can start an agent turn
|
||||
3. `preflight` -- adapter does dedupe, self-echo, hydration, debounce, decryption, partial fact prefill
|
||||
4. `resolve` -- adapter returns a fully assembled turn (route, reply plan, message, delivery)
|
||||
5. `authorize` -- DM, group, mention, and command policy applied to the assembled facts
|
||||
6. `assemble` -- `FinalizedMsgContext` built from the facts via `buildContext`
|
||||
7. `record` -- inbound session metadata and last route persisted
|
||||
8. `dispatch` -- agent turn executed through the buffered block dispatcher
|
||||
9. `finalize` -- adapter `onFinalize` runs even on dispatch error
|
||||
|
||||
Each stage emits a structured log event when a `log` callback is supplied. See [Observability](#observability).
|
||||
|
||||
## Admission kinds
|
||||
|
||||
The kernel does not throw when a turn is gated. It returns a `ChannelTurnAdmission`:
|
||||
|
||||
| Kind | When |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `dispatch` | Turn is admitted. Agent turn runs and the visible reply path is exercised. |
|
||||
| `observeOnly` | Turn runs end-to-end but the delivery adapter sends nothing visible. Used for broadcast observer agents and other passive multi-agent flows. |
|
||||
| `handled` | A platform event was consumed locally (lifecycle, reaction, button, modal). Kernel skips dispatch. |
|
||||
| `drop` | Skip path. Optionally `recordHistory: true` keeps the message in pending group history so a future mention has context. |
|
||||
|
||||
Admission can come from `classify` (event class said it cannot start a turn), from `preflight` (dedupe, self-echo, missing mention with history record), or from `resolveTurn` itself.
|
||||
|
||||
## Entry points
|
||||
|
||||
The runtime exposes three preferred entry points so adapters can opt in at the level that matches the channel.
|
||||
|
||||
```typescript
|
||||
runtime.channel.turn.run(...) // adapter-driven full pipeline
|
||||
runtime.channel.turn.runPrepared(...) // channel owns dispatch; kernel runs record + finalize
|
||||
runtime.channel.turn.buildContext(...) // pure facts to FinalizedMsgContext mapping
|
||||
```
|
||||
|
||||
Two older runtime helpers remain available for Plugin SDK compatibility:
|
||||
|
||||
```typescript
|
||||
runtime.channel.turn.runResolved(...) // deprecated compatibility alias; prefer run
|
||||
runtime.channel.turn.dispatchAssembled(...) // deprecated compatibility alias; prefer run or runPrepared
|
||||
```
|
||||
|
||||
### run
|
||||
|
||||
Use when your channel can express its inbound flow as a `ChannelTurnAdapter<TRaw>`. The adapter has callbacks for `ingest`, optional `classify`, optional `preflight`, mandatory `resolveTurn`, and optional `onFinalize`.
|
||||
|
||||
```typescript
|
||||
await runtime.channel.turn.run({
|
||||
channel: "tlon",
|
||||
accountId,
|
||||
raw: platformEvent,
|
||||
adapter: {
|
||||
ingest(raw) {
|
||||
return {
|
||||
id: raw.messageId,
|
||||
timestamp: raw.timestamp,
|
||||
rawText: raw.body,
|
||||
textForAgent: raw.body,
|
||||
};
|
||||
},
|
||||
classify(input) {
|
||||
return { kind: "message", canStartAgentTurn: input.rawText.length > 0 };
|
||||
},
|
||||
async preflight(input, eventClass) {
|
||||
if (await isDuplicate(input.id)) {
|
||||
return { admission: { kind: "drop", reason: "dedupe" } };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
resolveTurn(input) {
|
||||
return buildAssembledTurn(input);
|
||||
},
|
||||
onFinalize(result) {
|
||||
clearPendingGroupHistory(result);
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
`run` is the right shape when the channel has small adapter logic and benefits from owning the lifecycle through hooks.
|
||||
|
||||
### runPrepared
|
||||
|
||||
Use when the channel has a complex local dispatcher with previews, retries, edits, or thread bootstrap that must stay channel-owned. The kernel still records the inbound session before dispatch and surfaces a uniform `DispatchedChannelTurnResult`.
|
||||
|
||||
```typescript
|
||||
const { dispatchResult } = await runtime.channel.turn.runPrepared({
|
||||
channel: "matrix",
|
||||
accountId,
|
||||
routeSessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession,
|
||||
record: {
|
||||
onRecordError,
|
||||
updateLastRoute,
|
||||
},
|
||||
onPreDispatchFailure: async (err) => {
|
||||
await stopStatusReactions();
|
||||
},
|
||||
runDispatch: async () => {
|
||||
return await runMatrixOwnedDispatcher();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Rich channels (Matrix, Mattermost, Microsoft Teams, Feishu, QQ Bot) use `runPrepared` because their dispatcher orchestrates platform-specific behavior the kernel must not learn about.
|
||||
|
||||
### buildContext
|
||||
|
||||
A pure function that maps fact bundles into `FinalizedMsgContext`. Use it when your channel hand-rolls part of the pipeline but wants consistent context shape.
|
||||
|
||||
```typescript
|
||||
const ctxPayload = runtime.channel.turn.buildContext({
|
||||
channel: "googlechat",
|
||||
accountId,
|
||||
messageId,
|
||||
timestamp,
|
||||
from,
|
||||
sender,
|
||||
conversation,
|
||||
route,
|
||||
reply,
|
||||
message,
|
||||
access,
|
||||
media,
|
||||
supplemental,
|
||||
});
|
||||
```
|
||||
|
||||
`buildContext` is also useful inside `resolveTurn` callbacks when assembling a turn for `run`.
|
||||
|
||||
<Note>
|
||||
Deprecated SDK helpers such as `dispatchInboundReplyWithBase` still bridge through an assembled-turn helper. New plugin code should use `run` or `runPrepared`.
|
||||
</Note>
|
||||
|
||||
## Fact types
|
||||
|
||||
The facts the kernel consumes from your adapter are platform-agnostic. Translate platform objects into these shapes before handing them to the kernel.
|
||||
|
||||
### NormalizedTurnInput
|
||||
|
||||
| Field | Purpose |
|
||||
| ----------------- | ---------------------------------------------------------------------------- |
|
||||
| `id` | Stable message id used for dedupe and logs |
|
||||
| `timestamp` | Optional epoch ms |
|
||||
| `rawText` | Body as received from the platform |
|
||||
| `textForAgent` | Optional cleaned body for the agent (mention strip, typing trim) |
|
||||
| `textForCommands` | Optional body used for `/command` parsing |
|
||||
| `raw` | Optional pass-through reference for adapter callbacks that need the original |
|
||||
|
||||
### ChannelEventClass
|
||||
|
||||
| Field | Purpose |
|
||||
| ---------------------- | ----------------------------------------------------------------------- |
|
||||
| `kind` | `message`, `command`, `interaction`, `reaction`, `lifecycle`, `unknown` |
|
||||
| `canStartAgentTurn` | If false the kernel returns `{ kind: "handled" }` |
|
||||
| `requiresImmediateAck` | Hint for adapters that need to ACK before dispatch |
|
||||
|
||||
### SenderFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| -------------- | -------------------------------------------------------------- |
|
||||
| `id` | Stable platform sender id |
|
||||
| `name` | Display name |
|
||||
| `username` | Handle if distinct from `name` |
|
||||
| `tag` | Discord-style discriminator or platform tag |
|
||||
| `roles` | Role ids, used for member-role allowlist matching |
|
||||
| `isBot` | True when the sender is a known bot (kernel uses for dropping) |
|
||||
| `isSelf` | True when the sender is the configured agent itself |
|
||||
| `displayLabel` | Pre-rendered label for envelope text |
|
||||
|
||||
### ConversationFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| ----------------- | -------------------------------------------------------------------- |
|
||||
| `kind` | `direct`, `group`, or `channel` |
|
||||
| `id` | Conversation id used for routing |
|
||||
| `label` | Human label for the envelope |
|
||||
| `spaceId` | Optional outer space identifier (Slack workspace, Matrix homeserver) |
|
||||
| `parentId` | Outer conversation id when this is a thread |
|
||||
| `threadId` | Thread id when this message is inside a thread |
|
||||
| `nativeChannelId` | Platform-native channel id when different from the routing id |
|
||||
| `routePeer` | Peer used for `resolveAgentRoute` lookup |
|
||||
|
||||
### RouteFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| ----------------------- | ---------------------------------------------------------- |
|
||||
| `agentId` | Agent that should handle this turn |
|
||||
| `accountId` | Optional override (multi-account channels) |
|
||||
| `routeSessionKey` | Session key used for routing |
|
||||
| `dispatchSessionKey` | Session key used at dispatch when different from route key |
|
||||
| `persistedSessionKey` | Session key written to persisted session metadata |
|
||||
| `parentSessionKey` | Parent for branched/threaded sessions |
|
||||
| `modelParentSessionKey` | Model-side parent for branched sessions |
|
||||
| `mainSessionKey` | Main DM owner pin for direct conversations |
|
||||
| `createIfMissing` | Allow record step to create a missing session row |
|
||||
|
||||
### ReplyPlanFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| `to` | Logical reply target written into context `To` |
|
||||
| `originatingTo` | Originating context target (`OriginatingTo`) |
|
||||
| `nativeChannelId` | Platform-native channel id for delivery |
|
||||
| `replyTarget` | Final visible-reply destination if it differs from `to` |
|
||||
| `deliveryTarget` | Lower-level delivery override |
|
||||
| `replyToId` | Quoted/anchored message id |
|
||||
| `replyToIdFull` | Full-form quoted id when the platform has both |
|
||||
| `messageThreadId` | Thread id at delivery time |
|
||||
| `threadParentId` | Parent message id of the thread |
|
||||
| `sourceReplyDeliveryMode` | `thread`, `reply`, `channel`, `direct`, or `none` |
|
||||
|
||||
### AccessFacts
|
||||
|
||||
`AccessFacts` carries the booleans the authorize stage needs. Identity matching stays in the channel: the kernel only consumes the result.
|
||||
|
||||
| Field | Purpose |
|
||||
| ---------- | ------------------------------------------------------------------------- |
|
||||
| `dm` | DM allow/pairing/deny decision and `allowFrom` list |
|
||||
| `group` | Group policy, route allow, sender allow, allowlist, mention requirement |
|
||||
| `commands` | Command authorization across configured authorizers |
|
||||
| `mentions` | Whether mention detection is possible and whether the agent was mentioned |
|
||||
|
||||
### MessageFacts
|
||||
|
||||
| Field | Purpose |
|
||||
| ---------------- | -------------------------------------------------------------- |
|
||||
| `body` | Final envelope body (formatted) |
|
||||
| `rawBody` | Raw inbound body |
|
||||
| `bodyForAgent` | Body the agent sees |
|
||||
| `commandBody` | Body used for command parsing |
|
||||
| `envelopeFrom` | Pre-rendered sender label for the envelope |
|
||||
| `senderLabel` | Optional override for the rendered sender |
|
||||
| `preview` | Short redacted preview for logs |
|
||||
| `inboundHistory` | Recent inbound history entries when the channel keeps a buffer |
|
||||
|
||||
### SupplementalContextFacts
|
||||
|
||||
Supplemental context covers quote, forwarded, and thread-bootstrap context. The kernel applies the configured `contextVisibility` policy. The channel adapter only provides facts and `senderAllowed` flags so cross-channel policy stays consistent.
|
||||
|
||||
### InboundMediaFacts
|
||||
|
||||
Media is fact-shaped. Platform download, auth, SSRF policy, CDN rules, and decryption stay channel-local. The kernel maps facts into `MediaPath`, `MediaUrl`, `MediaType`, `MediaPaths`, `MediaUrls`, `MediaTypes`, and `MediaTranscribedIndexes`.
|
||||
|
||||
## Adapter contract
|
||||
|
||||
For full `run`, the adapter shape is:
|
||||
|
||||
```typescript
|
||||
type ChannelTurnAdapter<TRaw> = {
|
||||
ingest(raw: TRaw): Promise<NormalizedTurnInput | null> | NormalizedTurnInput | null;
|
||||
classify?(input: NormalizedTurnInput): Promise<ChannelEventClass> | ChannelEventClass;
|
||||
preflight?(
|
||||
input: NormalizedTurnInput,
|
||||
eventClass: ChannelEventClass,
|
||||
): Promise<PreflightFacts | ChannelTurnAdmission | null | undefined>;
|
||||
resolveTurn(
|
||||
input: NormalizedTurnInput,
|
||||
eventClass: ChannelEventClass,
|
||||
preflight: PreflightFacts,
|
||||
): Promise<ChannelTurnResolved> | ChannelTurnResolved;
|
||||
onFinalize?(result: ChannelTurnResult): Promise<void> | void;
|
||||
};
|
||||
```
|
||||
|
||||
`resolveTurn` returns a `ChannelTurnResolved`, which is an `AssembledChannelTurn` with an optional admission kind. Returning `{ admission: { kind: "observeOnly" } }` runs the turn without producing visible output. The adapter still owns the delivery callback; it just becomes a no-op for that turn.
|
||||
|
||||
`onFinalize` runs on every result, including dispatch errors. Use it to clear pending group history, remove ack reactions, stop status indicators, and flush local state.
|
||||
|
||||
## Delivery adapter
|
||||
|
||||
The kernel does not call the platform directly. The channel hands the kernel a `ChannelTurnDeliveryAdapter`:
|
||||
|
||||
```typescript
|
||||
type ChannelTurnDeliveryAdapter = {
|
||||
deliver(payload: ReplyPayload, info: ChannelDeliveryInfo): Promise<ChannelDeliveryResult | void>;
|
||||
onError?(err: unknown, info: { kind: string }): void;
|
||||
};
|
||||
|
||||
type ChannelDeliveryResult = {
|
||||
messageIds?: string[];
|
||||
threadId?: string;
|
||||
replyToId?: string;
|
||||
visibleReplySent?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
`deliver` is called once per buffered reply chunk. Return platform message ids when the channel has them so the dispatcher can preserve thread anchors and edit later chunks. For observe-only turns, return `{ visibleReplySent: false }` or use `createNoopChannelTurnDeliveryAdapter()`.
|
||||
|
||||
## Record options
|
||||
|
||||
The record stage wraps `recordInboundSession`. Most channels can use the defaults. Override via `record`:
|
||||
|
||||
```typescript
|
||||
record: {
|
||||
groupResolution,
|
||||
createIfMissing: true,
|
||||
updateLastRoute,
|
||||
onRecordError: (err) => log.warn("record failed", err),
|
||||
trackSessionMetaTask: (task) => pendingTasks.push(task),
|
||||
}
|
||||
```
|
||||
|
||||
The dispatcher waits for the record stage. If record throws, the kernel runs `onPreDispatchFailure` (when provided to `runPrepared`) and rethrows.
|
||||
|
||||
## Observability
|
||||
|
||||
Each stage emits a structured event when a `log` callback is supplied:
|
||||
|
||||
```typescript
|
||||
await runtime.channel.turn.run({
|
||||
channel: "twitch",
|
||||
accountId,
|
||||
raw,
|
||||
adapter,
|
||||
log: (event) => {
|
||||
runtime.log?.debug?.(`turn.${event.stage}:${event.event}`, {
|
||||
channel: event.channel,
|
||||
accountId: event.accountId,
|
||||
messageId: event.messageId,
|
||||
sessionKey: event.sessionKey,
|
||||
admission: event.admission,
|
||||
reason: event.reason,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Logged stages: `ingest`, `classify`, `preflight`, `resolve`, `authorize`, `assemble`, `record`, `dispatch`, `finalize`. Avoid logging raw bodies; use `MessageFacts.preview` for short redacted previews.
|
||||
|
||||
## What stays channel-local
|
||||
|
||||
The kernel owns orchestration. The channel still owns:
|
||||
|
||||
- Platform transports (gateway, REST, websocket, polling, webhooks)
|
||||
- Identity resolution and display-name matching
|
||||
- Native commands, slash commands, autocomplete, modals, buttons, voice state
|
||||
- Card, modal, and adaptive-card rendering
|
||||
- Media auth, CDN rules, encrypted media, transcription
|
||||
- Edit, reaction, redaction, and presence APIs
|
||||
- Backfill and platform-side history fetch
|
||||
- Pairing flows that require platform-specific verification
|
||||
|
||||
If two channels start needing the same helper for one of these, extract a shared SDK helper instead of pushing it into the kernel.
|
||||
|
||||
## Stability
|
||||
|
||||
`runtime.channel.turn.*` is part of the public plugin runtime surface. The fact types (`SenderFacts`, `ConversationFacts`, `RouteFacts`, `ReplyPlanFacts`, `AccessFacts`, `MessageFacts`, `SupplementalContextFacts`, `InboundMediaFacts`) and admission shapes (`ChannelTurnAdmission`, `ChannelEventClass`) are reachable through `PluginRuntime` from `openclaw/plugin-sdk/core`.
|
||||
|
||||
Backward compatibility rules apply: new fact fields are additive, admission kinds are not renamed, and the entry point names stay stable. New channel needs that require a non-additive change must go through the plugin SDK migration process.
|
||||
|
||||
## Related
|
||||
|
||||
- [Building channel plugins](/plugins/sdk-channel-plugins) for the broader channel plugin contract
|
||||
- [Plugin runtime helpers](/plugins/sdk-runtime) for other `runtime.*` surfaces
|
||||
- [Plugin internals](/plugins/architecture-internals) for load pipeline and registry mechanics
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
summary: "Import map, registration API reference, and SDK architecture"
|
||||
title: "Plugin SDK overview"
|
||||
sidebarTitle: "Plugin SDK overview"
|
||||
sidebarTitle: "SDK overview"
|
||||
read_when:
|
||||
- You need to know which SDK subpath to import from
|
||||
- You want a reference for all registration methods on OpenClawPluginApi
|
||||
@@ -11,14 +11,6 @@ read_when:
|
||||
The plugin SDK is the typed contract between plugins and core. This page is the
|
||||
reference for **what to import** and **what you can register**.
|
||||
|
||||
<Note>
|
||||
This page is for plugin authors using `openclaw/plugin-sdk/*` inside
|
||||
OpenClaw. For external apps, scripts, dashboards, CI jobs, and IDE extensions
|
||||
that want to run agents through the Gateway, use the
|
||||
[OpenClaw App SDK](/concepts/openclaw-sdk) and the `@openclaw/sdk` package
|
||||
instead.
|
||||
</Note>
|
||||
|
||||
<Tip>
|
||||
Looking for a how-to guide instead? Start with [Building plugins](/plugins/building-plugins), use [Channel plugins](/plugins/sdk-channel-plugins) for channel plugins, [Provider plugins](/plugins/sdk-provider-plugins) for provider plugins, and [Plugin hooks](/plugins/hooks) for tool or lifecycle hook plugins.
|
||||
</Tip>
|
||||
@@ -354,7 +346,7 @@ Facade-loaded bundled plugin public surfaces (`api.ts`, `runtime-api.ts`,
|
||||
`index.ts`, `setup-entry.ts`, and similar public entry files) prefer the
|
||||
active runtime config snapshot when OpenClaw is already running. If no runtime
|
||||
snapshot exists yet, they fall back to the resolved config file on disk.
|
||||
Packaged bundled plugin facades should be loaded through OpenClaw's plugin
|
||||
Packaged bundled plugin facades should be loaded through the OpenClaw SDK
|
||||
facade loaders; direct imports from `dist/extensions/...` bypass staged runtime
|
||||
dependency mirrors that packaged installs use for plugin-owned dependencies.
|
||||
|
||||
|
||||
@@ -178,9 +178,7 @@ Provider and channel execution paths must use the active runtime config snapshot
|
||||
});
|
||||
```
|
||||
|
||||
Inside the Gateway this runtime is in-process. In plugin CLI commands it calls the configured Gateway over RPC, so commands such as `openclaw googlemeet recover-tab` can inspect paired nodes from the terminal. Node commands still go through normal Gateway node pairing, command allowlists, plugin node-invoke policies, and node-local command handling.
|
||||
|
||||
Plugins that expose dangerous node-host commands should register a node-invoke policy with `api.registerNodeInvokePolicy(...)`. The policy runs in the Gateway after command allowlist checks and before the command is forwarded to the node, so direct `node.invoke` calls and higher-level plugin tools share the same enforcement path.
|
||||
Inside the Gateway this runtime is in-process. In plugin CLI commands it calls the configured Gateway over RPC, so commands such as `openclaw googlemeet recover-tab` can inspect paired nodes from the terminal. Node commands still go through normal Gateway node pairing, command allowlists, and node-local command handling.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="api.runtime.tasks.managedFlows">
|
||||
|
||||
@@ -86,7 +86,6 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/direct-dm` | Shared direct-DM auth/guard helpers |
|
||||
| `plugin-sdk/discord` | Deprecated Discord compatibility facade for published `@openclaw/discord@2026.3.13` and tracked owner compatibility; new plugins should use generic channel SDK subpaths |
|
||||
| `plugin-sdk/telegram-account` | Deprecated Telegram account-resolution compatibility facade for tracked owner compatibility; new plugins should use injected runtime helpers or generic channel SDK subpaths |
|
||||
| `plugin-sdk/zalouser` | Deprecated Zalo Personal compatibility facade for published Lark/Zalo packages that still import sender command authorization; new plugins should use `plugin-sdk/command-auth` |
|
||||
| `plugin-sdk/interactive-runtime` | Semantic message presentation, delivery, and legacy interactive reply helpers. See [Message Presentation](/plugins/message-presentation) |
|
||||
| `plugin-sdk/channel-inbound` | Compatibility barrel for inbound debounce, mention matching, mention-policy helpers, and envelope helpers |
|
||||
| `plugin-sdk/channel-inbound-debounce` | Narrow inbound debounce helpers |
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
---
|
||||
summary: "Reference design for the public OpenClaw App SDK API, event taxonomy, artifacts, approvals, and package structure"
|
||||
title: "OpenClaw App SDK API design"
|
||||
sidebarTitle: "App SDK API design"
|
||||
read_when:
|
||||
- You are implementing the proposed public OpenClaw app SDK
|
||||
- You need the draft namespace, event, result, artifact, approval, or security contract for the app SDK
|
||||
- You are comparing Gateway protocol resources with the high-level OpenClaw App SDK wrapper
|
||||
---
|
||||
|
||||
This page is the detailed API reference design for the public
|
||||
[OpenClaw App SDK](/concepts/openclaw-sdk). It is intentionally separate from
|
||||
the [Plugin SDK](/plugins/sdk-overview).
|
||||
|
||||
<Note>
|
||||
`@openclaw/sdk` is the external app/client package for talking to the
|
||||
Gateway. `openclaw/plugin-sdk/*` is the in-process plugin authoring contract.
|
||||
Do not import Plugin SDK subpaths from apps that only need to run agents.
|
||||
</Note>
|
||||
|
||||
The public app SDK should be built in two layers:
|
||||
|
||||
1. A low-level generated Gateway client.
|
||||
2. A high-level ergonomic wrapper with `OpenClaw`, `Agent`, `Session`, `Run`,
|
||||
`Task`, `Artifact`, `Approval`, and `Environment` objects.
|
||||
|
||||
## Namespace design
|
||||
|
||||
The low-level namespaces should closely follow Gateway resources:
|
||||
|
||||
```typescript
|
||||
oc.agents.list();
|
||||
oc.agents.get("main");
|
||||
oc.agents.create(...);
|
||||
oc.agents.update(...);
|
||||
|
||||
oc.sessions.list();
|
||||
oc.sessions.create(...);
|
||||
oc.sessions.resolve(...);
|
||||
oc.sessions.send(...);
|
||||
oc.sessions.messages(...);
|
||||
oc.sessions.fork(...);
|
||||
oc.sessions.compact(...);
|
||||
oc.sessions.abort(...);
|
||||
|
||||
oc.runs.create(...);
|
||||
oc.runs.get(runId);
|
||||
oc.runs.events(runId, { after });
|
||||
oc.runs.wait(runId);
|
||||
oc.runs.cancel(runId);
|
||||
|
||||
oc.tasks.list(); // future API: current SDK throws unsupported
|
||||
oc.tasks.get(taskId); // future API: current SDK throws unsupported
|
||||
oc.tasks.cancel(taskId); // future API: current SDK throws unsupported
|
||||
oc.tasks.events(taskId, { after }); // future API
|
||||
|
||||
oc.models.list();
|
||||
oc.models.status(); // Gateway models.authStatus
|
||||
|
||||
oc.tools.list();
|
||||
oc.tools.invoke(...); // future API: current SDK throws unsupported
|
||||
|
||||
oc.artifacts.list({ runId }); // future API: current SDK throws unsupported
|
||||
oc.artifacts.get(artifactId); // future API: current SDK throws unsupported
|
||||
oc.artifacts.download(artifactId); // future API: current SDK throws unsupported
|
||||
|
||||
oc.approvals.list();
|
||||
oc.approvals.respond(approvalId, ...);
|
||||
|
||||
oc.environments.list(); // future API: current SDK throws unsupported
|
||||
oc.environments.create(...); // future API: current SDK throws unsupported
|
||||
oc.environments.status(environmentId); // future API: current SDK throws unsupported
|
||||
oc.environments.delete(environmentId); // future API: current SDK throws unsupported
|
||||
```
|
||||
|
||||
High-level wrappers should return objects that make common flows pleasant:
|
||||
|
||||
```typescript
|
||||
const run = await agent.run(inputOrParams);
|
||||
await run.cancel();
|
||||
await run.wait();
|
||||
|
||||
for await (const event of run.events()) {
|
||||
// normalized event stream
|
||||
}
|
||||
|
||||
const artifacts = await run.artifacts.list();
|
||||
const session = await run.session();
|
||||
```
|
||||
|
||||
## Event contract
|
||||
|
||||
The public SDK should expose versioned, replayable, normalized events.
|
||||
|
||||
```typescript
|
||||
type OpenClawEvent = {
|
||||
version: 1;
|
||||
id: string;
|
||||
ts: number;
|
||||
type: OpenClawEventType;
|
||||
runId?: string;
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
taskId?: string;
|
||||
agentId?: string;
|
||||
data: unknown;
|
||||
raw?: unknown;
|
||||
};
|
||||
```
|
||||
|
||||
`id` is a replay cursor. Consumers should be able to reconnect with
|
||||
`events({ after: id })` and receive missed events when retention allows.
|
||||
|
||||
Recommended normalized event families:
|
||||
|
||||
| Event | Meaning |
|
||||
| --------------------- | ----------------------------------------------------------- |
|
||||
| `run.created` | Run accepted. |
|
||||
| `run.queued` | Run is waiting for a session lane, runtime, or environment. |
|
||||
| `run.started` | Runtime started execution. |
|
||||
| `run.completed` | Run finished successfully. |
|
||||
| `run.failed` | Run ended with an error. |
|
||||
| `run.cancelled` | Run was cancelled. |
|
||||
| `run.timed_out` | Run exceeded its timeout. |
|
||||
| `assistant.delta` | Assistant text delta. |
|
||||
| `assistant.message` | Complete assistant message or replacement. |
|
||||
| `thinking.delta` | Reasoning or plan delta, when policy allows exposure. |
|
||||
| `tool.call.started` | Tool call began. |
|
||||
| `tool.call.delta` | Tool call streamed progress or partial output. |
|
||||
| `tool.call.completed` | Tool call returned successfully. |
|
||||
| `tool.call.failed` | Tool call failed. |
|
||||
| `approval.requested` | A run or tool needs approval. |
|
||||
| `approval.resolved` | Approval was granted, denied, expired, or cancelled. |
|
||||
| `question.requested` | Runtime asks the user or host app for input. |
|
||||
| `question.answered` | Host app supplied an answer. |
|
||||
| `artifact.created` | New artifact available. |
|
||||
| `artifact.updated` | Existing artifact changed. |
|
||||
| `session.created` | Session created. |
|
||||
| `session.updated` | Session metadata changed. |
|
||||
| `session.compacted` | Session compaction happened. |
|
||||
| `task.updated` | Background task state changed. |
|
||||
| `git.branch` | Runtime observed or changed branch state. |
|
||||
| `git.diff` | Runtime produced or changed a diff. |
|
||||
| `git.pr` | Runtime opened, updated, or linked a pull request. |
|
||||
|
||||
Runtime-native payloads should be available through `raw`, but apps should not
|
||||
have to parse `raw` for normal UI.
|
||||
|
||||
## Result contract
|
||||
|
||||
`Run.wait()` should return a stable result envelope:
|
||||
|
||||
```typescript
|
||||
type RunResult = {
|
||||
runId: string;
|
||||
status: "accepted" | "completed" | "failed" | "cancelled" | "timed_out";
|
||||
sessionId?: string;
|
||||
sessionKey?: string;
|
||||
taskId?: string;
|
||||
startedAt?: string | number;
|
||||
endedAt?: string | number;
|
||||
output?: {
|
||||
text?: string;
|
||||
messages?: SDKMessage[];
|
||||
};
|
||||
usage?: {
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalTokens?: number;
|
||||
costUsd?: number;
|
||||
};
|
||||
artifacts?: ArtifactSummary[];
|
||||
error?: SDKError;
|
||||
};
|
||||
```
|
||||
|
||||
The result should be boring and stable. Timestamp values preserve the Gateway
|
||||
shape, so current lifecycle-backed runs usually report epoch millisecond
|
||||
numbers while adapters may still surface ISO strings. Rich UI, tool traces, and
|
||||
runtime-native details belong in events and artifacts.
|
||||
|
||||
`accepted` is a non-terminal wait result: it means the Gateway wait deadline
|
||||
expired before the run produced a lifecycle end/error. It must not be treated as
|
||||
`timed_out`; `timed_out` is reserved for a run that exceeded its own runtime
|
||||
timeout.
|
||||
|
||||
## Approvals and questions
|
||||
|
||||
Approvals must be first-class because coding agents constantly cross safety
|
||||
boundaries.
|
||||
|
||||
```typescript
|
||||
run.onApproval(async (request) => {
|
||||
if (request.kind === "tool" && request.toolName === "exec") {
|
||||
return request.approveOnce({ reason: "CI command allowed by policy" });
|
||||
}
|
||||
|
||||
return request.askUser();
|
||||
});
|
||||
```
|
||||
|
||||
Approval events should carry:
|
||||
|
||||
- approval id
|
||||
- run id and session id
|
||||
- request kind
|
||||
- requested action summary
|
||||
- tool name or environment action
|
||||
- risk level
|
||||
- available decisions
|
||||
- expiration
|
||||
- whether the decision can be reused
|
||||
|
||||
Questions are separate from approvals. A question asks the user or host app for
|
||||
information. An approval asks for permission to perform an action.
|
||||
|
||||
## ToolSpace model
|
||||
|
||||
Apps need to understand the tool surface without importing plugin internals.
|
||||
|
||||
```typescript
|
||||
const tools = await run.toolSpace();
|
||||
|
||||
for (const tool of tools.list()) {
|
||||
console.log(tool.name, tool.source, tool.requiresApproval);
|
||||
}
|
||||
```
|
||||
|
||||
The SDK should expose:
|
||||
|
||||
- normalized tool metadata
|
||||
- source: OpenClaw, MCP, plugin, channel, runtime, or app
|
||||
- schema summary
|
||||
- approval policy
|
||||
- runtime compatibility
|
||||
- whether a tool is hidden, readonly, write capable, or host capable
|
||||
|
||||
Tool invocation through the SDK should be explicit and scoped. Most apps should
|
||||
run agents, not call arbitrary tools directly.
|
||||
|
||||
## Artifact model
|
||||
|
||||
Artifacts should cover more than files.
|
||||
|
||||
```typescript
|
||||
type ArtifactSummary = {
|
||||
id: string;
|
||||
runId?: string;
|
||||
sessionId?: string;
|
||||
type:
|
||||
| "file"
|
||||
| "patch"
|
||||
| "diff"
|
||||
| "log"
|
||||
| "media"
|
||||
| "screenshot"
|
||||
| "trajectory"
|
||||
| "pull_request"
|
||||
| "workspace";
|
||||
title?: string;
|
||||
mimeType?: string;
|
||||
sizeBytes?: number;
|
||||
createdAt: string;
|
||||
expiresAt?: string;
|
||||
};
|
||||
```
|
||||
|
||||
Common examples:
|
||||
|
||||
- file edits and generated files
|
||||
- patch bundles
|
||||
- VCS diffs
|
||||
- screenshots and media outputs
|
||||
- logs and trace bundles
|
||||
- pull request links
|
||||
- runtime trajectories
|
||||
- managed environment workspace snapshots
|
||||
|
||||
Artifact access should support redaction, retention, and download URLs without
|
||||
assuming every artifact is a normal local file.
|
||||
|
||||
## Security model
|
||||
|
||||
The app SDK must be explicit about authority.
|
||||
|
||||
Recommended token scopes:
|
||||
|
||||
| Scope | Allows |
|
||||
| ------------------- | --------------------------------------------------- |
|
||||
| `agent.read` | List and inspect agents. |
|
||||
| `agent.run` | Start runs. |
|
||||
| `session.read` | Read session metadata and messages. |
|
||||
| `session.write` | Create, send to, fork, compact, and abort sessions. |
|
||||
| `task.read` | Read background task state. |
|
||||
| `task.write` | Cancel or modify task notification policy. |
|
||||
| `approval.respond` | Approve or deny requests. |
|
||||
| `tools.invoke` | Invoke exposed tools directly. |
|
||||
| `artifacts.read` | List and download artifacts. |
|
||||
| `environment.write` | Create or destroy managed environments. |
|
||||
| `admin` | Administrative operations. |
|
||||
|
||||
Defaults:
|
||||
|
||||
- no secret forwarding by default
|
||||
- no unrestricted environment variable pass-through
|
||||
- secret references instead of secret values
|
||||
- explicit sandbox and network policy
|
||||
- explicit remote environment retention
|
||||
- approvals for host execution unless policy proves otherwise
|
||||
- raw runtime events redacted before they leave Gateway unless the caller has a
|
||||
stronger diagnostic scope
|
||||
|
||||
## Managed environment provider
|
||||
|
||||
Managed agents should be implemented as environment providers.
|
||||
|
||||
```typescript
|
||||
type EnvironmentProvider = {
|
||||
id: string;
|
||||
capabilities: {
|
||||
checkout?: boolean;
|
||||
sandbox?: boolean;
|
||||
networkPolicy?: boolean;
|
||||
secrets?: boolean;
|
||||
artifacts?: boolean;
|
||||
logs?: boolean;
|
||||
pullRequests?: boolean;
|
||||
longRunning?: boolean;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
The first implementation does not need to be a hosted SaaS. It can target
|
||||
existing node hosts, ephemeral workspaces, CI-style runners, or Testbox-style
|
||||
environments. The important contract is:
|
||||
|
||||
1. prepare workspace
|
||||
2. bind safe environment and secrets
|
||||
3. start run
|
||||
4. stream events
|
||||
5. collect artifacts
|
||||
6. clean up or retain by policy
|
||||
|
||||
Once this is stable, a hosted cloud service can implement the same provider
|
||||
contract.
|
||||
|
||||
## Package structure
|
||||
|
||||
Recommended packages:
|
||||
|
||||
| Package | Purpose |
|
||||
| ----------------------- | ------------------------------------------------------------- |
|
||||
| `@openclaw/sdk` | Public high-level SDK and generated low-level Gateway client. |
|
||||
| `@openclaw/sdk-react` | Optional React hooks for dashboards and app builders. |
|
||||
| `@openclaw/sdk-testing` | Test helpers and fake Gateway server for app integrations. |
|
||||
|
||||
The repo already has `openclaw/plugin-sdk/*` for plugins. Keep that namespace
|
||||
separate to avoid confusing plugin authors with app developers.
|
||||
|
||||
## Generated client strategy
|
||||
|
||||
The low-level client should be generated from versioned Gateway protocol
|
||||
schemas, then wrapped by handwritten ergonomic classes.
|
||||
|
||||
Layering:
|
||||
|
||||
1. Gateway schema source of truth.
|
||||
2. Generated low-level TypeScript client.
|
||||
3. Runtime validators for external inputs and event payloads.
|
||||
4. High-level `OpenClaw`, `Agent`, `Session`, `Run`, `Task`, and `Artifact`
|
||||
wrappers.
|
||||
5. Cookbook examples and integration tests.
|
||||
|
||||
Benefits:
|
||||
|
||||
- protocol drift is visible
|
||||
- tests can compare generated methods with Gateway exports
|
||||
- App SDK stays independent from Plugin SDK internals
|
||||
- low-level consumers still have full protocol access
|
||||
- high-level consumers get the small product API
|
||||
|
||||
## Related docs
|
||||
|
||||
- [OpenClaw App SDK](/concepts/openclaw-sdk)
|
||||
- [Gateway RPC reference](/reference/rpc)
|
||||
- [Agent loop](/concepts/agent-loop)
|
||||
- [Agent runtimes](/concepts/agent-runtimes)
|
||||
- [Background tasks](/automation/tasks)
|
||||
- [ACP agents](/tools/acp-agents)
|
||||
- [Plugin SDK overview](/plugins/sdk-overview)
|
||||
@@ -38,26 +38,12 @@ OpenClaw process
|
||||
|
||||
The public contract is the routing behavior, not the internal Node hooks used to implement it. OpenClaw Gateway control-plane WebSocket clients use a narrow direct path for local loopback Gateway RPC traffic when the Gateway URL uses `localhost` or a literal loopback IP such as `127.0.0.1` or `[::1]`. That control-plane path must be able to reach loopback Gateways even when the operator proxy blocks loopback destinations. Normal runtime HTTP and WebSocket requests still use the configured proxy.
|
||||
|
||||
Internally, OpenClaw uses two process-level routing hooks for this feature:
|
||||
|
||||
- Undici dispatcher routing covers `fetch`, undici-backed clients, and transports that provide their own undici dispatcher.
|
||||
- `global-agent` routing covers Node core `node:http` and `node:https` callers, including many libraries layered on `http.request`, `https.request`, `http.get`, and `https.get`. Managed proxy mode forces that global agent so explicit Node HTTP agents do not accidentally bypass the operator proxy.
|
||||
|
||||
Some plugins own custom transports that need explicit proxy wiring even when process-level routing exists. For example, Telegram's Bot API transport uses its own HTTP/1 undici dispatcher and therefore honors process proxy env plus the managed `OPENCLAW_PROXY_URL` fallback in that owner-specific transport path.
|
||||
|
||||
The proxy URL itself must use `http://`. HTTPS destinations are still supported through the proxy with HTTP `CONNECT`; this only means OpenClaw expects a plain HTTP forward-proxy listener such as `http://127.0.0.1:3128`.
|
||||
|
||||
While the proxy is active, OpenClaw clears `no_proxy`, `NO_PROXY`, and `GLOBAL_AGENT_NO_PROXY`. Those bypass lists are destination-based, so leaving `localhost` or `127.0.0.1` there would let high-risk SSRF targets skip the filtering proxy.
|
||||
|
||||
On shutdown, OpenClaw restores the previous proxy environment and resets cached process routing state.
|
||||
|
||||
## Related Proxy Terms
|
||||
|
||||
- `proxy.enabled` / `proxy.proxyUrl`: outbound forward-proxy routing for OpenClaw runtime egress. This page documents that feature.
|
||||
- `gateway.auth.mode: "trusted-proxy"`: inbound identity-aware reverse-proxy authentication for Gateway access. See [Trusted proxy auth](/gateway/trusted-proxy-auth).
|
||||
- `openclaw proxy`: local debug proxy and capture inspector for development and support. See [openclaw proxy](/cli/proxy).
|
||||
- Channel or provider-specific proxy settings: owner-specific overrides for a particular transport. Prefer the managed network proxy when the goal is central egress control across the runtime.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -130,9 +130,6 @@ Native `openclaw skills install` installs into the active workspace
|
||||
`./skills` under your current working directory (or falls back to the
|
||||
configured OpenClaw workspace). OpenClaw picks that up as
|
||||
`<workspace>/skills` on the next session.
|
||||
Configured skill roots also support one grouping level, such as
|
||||
`skills/<group>/<skill>/SKILL.md`, so related third-party skills can be
|
||||
kept under a shared folder without broad recursive scanning.
|
||||
|
||||
ClawHub skill pages expose the latest security scan state before install,
|
||||
with scanner detail pages for VirusTotal, ClawScan, and static analysis.
|
||||
|
||||
@@ -142,7 +142,7 @@ Current source-of-truth:
|
||||
- `/exec host=<auto|sandbox|gateway|node> security=<deny|allowlist|full> ask=<off|on-miss|always> node=<id>` shows or sets exec defaults.
|
||||
- `/model [name|#|status]` shows or sets the model.
|
||||
- `/models [provider] [page] [limit=<n>|size=<n>|all]` lists configured/auth-available providers or models for a provider; add `all` to browse that provider's full catalog.
|
||||
- `/queue <mode>` manages queue behavior (`steer`, legacy `queue`, `followup`, `collect`, `steer-backlog`, `interrupt`) plus options like `debounce:0.5s cap:25 drop:summarize`; `/queue default` or `/queue reset` clears the session override. See [Command queue](/concepts/queue) and [Steering queue](/concepts/queue-steering).
|
||||
- `/queue <mode>` manages queue behavior (`steer`, `followup`, `collect`, `steer-backlog`, `interrupt`) plus options like `debounce:0.5s cap:25 drop:summarize`; `/queue default` or `/queue reset` clears the session override. See [Command queue](/concepts/queue).
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Discovery and status">
|
||||
|
||||
@@ -83,8 +83,6 @@ The Control UI can localize itself on first load based on your browser locale. T
|
||||
- The selected locale is saved in browser storage and reused on future visits.
|
||||
- Missing translation keys fall back to English.
|
||||
|
||||
Docs translations are generated for the same non-English locale set, but the docs site's built-in Mintlify language picker is limited to the locale codes Mintlify accepts. Thai (`th`) and Persian (`fa`) docs are still generated in the publish repo; they may not appear in that picker until Mintlify supports those codes.
|
||||
|
||||
## Appearance themes
|
||||
|
||||
The Appearance panel keeps the built-in Claw, Knot, and Dash themes, plus one browser-local tweakcn import slot. To import a theme, open [tweakcn themes](https://tweakcn.com/themes), choose or create a theme, click **Share**, and paste the copied theme link into Appearance. The importer also accepts `https://tweakcn.com/r/themes/<id>` registry URLs, editor URLs like `https://tweakcn.com/editor/theme?theme=amethyst-haze`, relative `/themes/<id>` paths, raw theme IDs, and default theme names such as `amethyst-haze`.
|
||||
|
||||
@@ -177,10 +177,10 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
});
|
||||
|
||||
const wrapper = await fs.readFile(generated.wrapperPath, "utf8");
|
||||
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.1"');
|
||||
expect(wrapper).toContain('"@agentclientprotocol/claude-agent-acp@0.31.0"');
|
||||
expect(wrapper).toContain('"--", "claude-agent-acp"');
|
||||
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@^0.31.0");
|
||||
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.0");
|
||||
expect(wrapper).not.toContain("@agentclientprotocol/claude-agent-acp@0.31.1");
|
||||
});
|
||||
|
||||
it("uses the bundled Codex ACP dependency by default when it is installed", async () => {
|
||||
@@ -379,7 +379,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
rawConfig: {
|
||||
agents: {
|
||||
claude: {
|
||||
command: "npx -y @agentclientprotocol/claude-agent-acp@0.31.1 --permission-mode bypass",
|
||||
command: "npx -y @agentclientprotocol/claude-agent-acp@0.31.0 --permission-mode bypass",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -425,7 +425,7 @@ describe("prepareAcpxCodexAuthConfig", () => {
|
||||
const root = await makeTempDir();
|
||||
const stateDir = path.join(root, "state");
|
||||
const command =
|
||||
"node ./custom-claude-wrapper.mjs @agentclientprotocol/claude-agent-acp@0.31.1 --flag";
|
||||
"node ./custom-claude-wrapper.mjs @agentclientprotocol/claude-agent-acp@0.31.0 --flag";
|
||||
const pluginConfig = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
agents: {
|
||||
|
||||
@@ -7,7 +7,7 @@ const CODEX_ACP_PACKAGE = "@zed-industries/codex-acp";
|
||||
const CODEX_ACP_PACKAGE_RANGE = "^0.12.0";
|
||||
const CODEX_ACP_BIN = "codex-acp";
|
||||
const CLAUDE_ACP_PACKAGE = "@agentclientprotocol/claude-agent-acp";
|
||||
const CLAUDE_ACP_PACKAGE_VERSION = "0.31.1";
|
||||
const CLAUDE_ACP_PACKAGE_VERSION = "0.31.0";
|
||||
const CLAUDE_ACP_BIN = "claude-agent-acp";
|
||||
const RUN_CONFIGURED_COMMAND_SENTINEL = "--openclaw-run-configured";
|
||||
const requireFromHere = createRequire(import.meta.url);
|
||||
|
||||
@@ -19,7 +19,7 @@ describe("acpx package manifest", () => {
|
||||
|
||||
expect(packageJson.dependencies?.acpx).toBeDefined();
|
||||
expect(packageJson.dependencies?.["@zed-industries/codex-acp"]).toBe("0.12.0");
|
||||
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.31.1");
|
||||
expect(packageJson.dependencies?.["@agentclientprotocol/claude-agent-acp"]).toBe("0.31.0");
|
||||
expect(packageJson.devDependencies?.["@agentclientprotocol/claude-agent-acp"]).toBeUndefined();
|
||||
expect(packageJson.openclaw?.bundle?.stageRuntimeDependencies).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1384,22 +1384,6 @@ describe("active-memory plugin", () => {
|
||||
expect(api.logger.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("config.modelFallbackPolicy is deprecated"),
|
||||
);
|
||||
// #74587: deprecation warning must spell out the chain-resolution
|
||||
// semantics so operators don't read it as a promise of runtime failover.
|
||||
// The previous wording ("set config.modelFallback if you want a fallback
|
||||
// model") cost real users hours of debug time before they hit the source
|
||||
// and saw `getModelRef` only walks candidates once.
|
||||
const warnCalls = (api.logger.warn as ReturnType<typeof vi.fn>).mock.calls;
|
||||
const deprecationMessage = warnCalls
|
||||
.map(([first]) => (typeof first === "string" ? first : ""))
|
||||
.find((message) => message.includes("config.modelFallbackPolicy is deprecated"));
|
||||
expect(deprecationMessage).toBeDefined();
|
||||
// Positive: the warning describes chain-resolution last-resort behavior.
|
||||
expect(deprecationMessage).toContain("chain-resolution");
|
||||
expect(deprecationMessage).toContain("last-resort");
|
||||
// Negative: the warning explicitly disclaims runtime failover, since
|
||||
// that's the wrong mental model the previous wording invited.
|
||||
expect(deprecationMessage).toMatch(/NOT a runtime failover/i);
|
||||
});
|
||||
|
||||
it("does not use a built-in fallback model even when default-remote is configured", async () => {
|
||||
|
||||
@@ -2431,19 +2431,8 @@ export default definePluginEntry({
|
||||
let config = normalizePluginConfig(api.pluginConfig);
|
||||
const warnDeprecatedModelFallbackPolicy = (pluginConfig: unknown) => {
|
||||
if (hasDeprecatedModelFallbackPolicy(pluginConfig)) {
|
||||
// Wording matters here: the previous text ("set config.modelFallback
|
||||
// explicitly if you want a fallback model") read naturally as runtime
|
||||
// failover (model A errors → switch to model B), but `getModelRef`
|
||||
// only consults `modelFallback` as the *last candidate* in the
|
||||
// resolution chain after `config.model`, the current run's model,
|
||||
// and the agent's configured default have all resolved to nothing.
|
||||
// Surface the chain-resolution semantics directly so operators
|
||||
// don't waste debug cycles assuming runtime failover (#74587).
|
||||
api.logger.warn?.(
|
||||
"active-memory: config.modelFallbackPolicy is deprecated and no longer changes runtime behavior. " +
|
||||
"config.modelFallback is a chain-resolution last-resort (consulted only when config.model, " +
|
||||
"the current run's model, and the agent's configured default all resolve to nothing) — " +
|
||||
"it is NOT a runtime failover that substitutes a different model when the resolved model errors out.",
|
||||
"active-memory: config.modelFallbackPolicy is deprecated and no longer changes runtime behavior; set config.modelFallback explicitly if you want a fallback model",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
},
|
||||
"modelFallbackPolicy": {
|
||||
"label": "Model Fallback Policy",
|
||||
"help": "Deprecated compatibility field. modelFallback is only the chain-resolution last resort when no explicit plugin model, session model, or agent primary model resolves; it is not runtime failover."
|
||||
"help": "Deprecated compatibility field. Active Memory no longer uses a built-in fallback model; set modelFallback explicitly if you want a fallback."
|
||||
},
|
||||
"allowedChatTypes": {
|
||||
"label": "Allowed Chat Types",
|
||||
|
||||
@@ -253,36 +253,6 @@ describe("amazon-bedrock provider plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("mirrors Claude Opus 4.7 thinking levels for Bedrock model refs", async () => {
|
||||
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||
|
||||
for (const modelId of [
|
||||
"us.anthropic.claude-opus-4-7",
|
||||
"us.anthropic.claude-opus-4.7-v1:0",
|
||||
"eu.anthropic.claude-opus-4-7",
|
||||
"arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-opus-4-7",
|
||||
]) {
|
||||
expect(
|
||||
provider.resolveThinkingProfile?.({
|
||||
provider: "amazon-bedrock",
|
||||
modelId,
|
||||
} as never),
|
||||
).toMatchObject({
|
||||
levels: [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
{ id: "xhigh" },
|
||||
{ id: "adaptive" },
|
||||
{ id: "max" },
|
||||
],
|
||||
defaultLevel: "off",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("owns Anthropic-style replay policy for Claude Bedrock models", async () => {
|
||||
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||
|
||||
@@ -412,58 +382,6 @@ describe("amazon-bedrock provider plugin", () => {
|
||||
).toEqual({ maxTokens: 12 });
|
||||
});
|
||||
|
||||
it("preserves Bedrock Opus 4.7 max thinking in the final payload", async () => {
|
||||
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||
const wrapped = provider.wrapStreamFn?.({
|
||||
provider: "amazon-bedrock",
|
||||
modelId: "us.anthropic.claude-opus-4-7",
|
||||
streamFn: spyStreamFn,
|
||||
thinkingLevel: "max",
|
||||
} as never);
|
||||
|
||||
const result = wrapped?.(
|
||||
{
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
id: "us.anthropic.claude-opus-4-7",
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{ reasoning: "xhigh" } as never,
|
||||
) as Record<string, unknown> | undefined;
|
||||
const payload = {
|
||||
additionalModelRequestFields: {
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort: "xhigh" },
|
||||
},
|
||||
};
|
||||
|
||||
await (result?.onPayload as ((p: Record<string, unknown>) => unknown) | undefined)?.(payload);
|
||||
|
||||
expect(payload.additionalModelRequestFields.output_config).toEqual({ effort: "max" });
|
||||
});
|
||||
|
||||
it("keeps Bedrock Opus 4.7 xhigh thinking distinct from max", async () => {
|
||||
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||
const wrapped = provider.wrapStreamFn?.({
|
||||
provider: "amazon-bedrock",
|
||||
modelId: "us.anthropic.claude-opus-4-7",
|
||||
streamFn: spyStreamFn,
|
||||
thinkingLevel: "xhigh",
|
||||
} as never);
|
||||
|
||||
const result = wrapped?.(
|
||||
{
|
||||
api: "bedrock-converse-stream",
|
||||
provider: "amazon-bedrock",
|
||||
id: "us.anthropic.claude-opus-4-7",
|
||||
} as never,
|
||||
{ messages: [] } as never,
|
||||
{ reasoning: "xhigh" } as never,
|
||||
) as Record<string, unknown> | undefined;
|
||||
|
||||
expect(result).not.toHaveProperty("onPayload");
|
||||
});
|
||||
|
||||
it("classifies nested Bedrock deprecated-temperature validation as format failover", async () => {
|
||||
const provider = await registerSingleProviderPlugin(amazonBedrockPlugin);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { StreamFn } from "@mariozechner/pi-agent-core";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import type { OpenClawPluginApi, ProviderThinkingProfile } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import {
|
||||
ANTHROPIC_BY_MODEL_REPLAY_HOOKS,
|
||||
normalizeProviderId,
|
||||
@@ -285,34 +285,11 @@ function injectBedrockCachePoints(
|
||||
}
|
||||
}
|
||||
|
||||
function patchOpus47MaxThinkingEffort(payload: Record<string, unknown>): void {
|
||||
const fieldsValue = payload.additionalModelRequestFields;
|
||||
const fields =
|
||||
fieldsValue && typeof fieldsValue === "object" && !Array.isArray(fieldsValue)
|
||||
? (fieldsValue as Record<string, unknown>)
|
||||
: {};
|
||||
const outputConfigValue = fields.output_config;
|
||||
const outputConfig =
|
||||
outputConfigValue && typeof outputConfigValue === "object" && !Array.isArray(outputConfigValue)
|
||||
? (outputConfigValue as Record<string, unknown>)
|
||||
: {};
|
||||
outputConfig.effort = "max";
|
||||
fields.output_config = outputConfig;
|
||||
payload.additionalModelRequestFields = fields;
|
||||
}
|
||||
|
||||
export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
// Keep registration-local constants inside the function so partial module
|
||||
// initialization during test bootstrap cannot trip TDZ reads.
|
||||
const providerId = "amazon-bedrock";
|
||||
const claude46ModelRe = /claude-(?:opus|sonnet)-4(?:\.|-)6(?:$|[-.])/i;
|
||||
const baseClaudeThinkingLevels = [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
] as const satisfies ProviderThinkingProfile["levels"];
|
||||
// Match region from bedrock-runtime (Converse API) URLs.
|
||||
// e.g. https://bedrock-runtime.us-east-1.amazonaws.com
|
||||
const bedrockRegionRe = /bedrock-runtime\.([a-z0-9-]+)\.amazonaws\./;
|
||||
@@ -326,23 +303,6 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
const anthropicByModelReplayHooks = ANTHROPIC_BY_MODEL_REPLAY_HOOKS;
|
||||
const startupPluginConfig = (api.pluginConfig ?? {}) as AmazonBedrockPluginConfig;
|
||||
|
||||
function resolveBedrockClaudeThinkingProfile(modelId: string): ProviderThinkingProfile {
|
||||
const trimmed = modelId.trim();
|
||||
if (isOpus47BedrockModelRef(trimmed)) {
|
||||
return {
|
||||
levels: [...baseClaudeThinkingLevels, { id: "xhigh" }, { id: "adaptive" }, { id: "max" }],
|
||||
defaultLevel: "off",
|
||||
};
|
||||
}
|
||||
if (claude46ModelRe.test(trimmed)) {
|
||||
return {
|
||||
levels: [...baseClaudeThinkingLevels, { id: "adaptive" }],
|
||||
defaultLevel: "adaptive",
|
||||
};
|
||||
}
|
||||
return { levels: baseClaudeThinkingLevels };
|
||||
}
|
||||
|
||||
function resolveCurrentPluginConfig(
|
||||
config: OpenClawConfig | undefined,
|
||||
): AmazonBedrockPluginConfig | undefined {
|
||||
@@ -457,7 +417,7 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
},
|
||||
resolveConfigApiKey: ({ env }) => resolveBedrockConfigApiKey(env),
|
||||
...anthropicByModelReplayHooks,
|
||||
wrapStreamFn: ({ modelId, config, model, streamFn, thinkingLevel }) => {
|
||||
wrapStreamFn: ({ modelId, config, model, streamFn }) => {
|
||||
const currentGuardrail = resolveCurrentPluginConfig(config)?.guardrail;
|
||||
// Apply cache + guardrail wrapping.
|
||||
const wrapped =
|
||||
@@ -468,13 +428,12 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
const mayNeedCacheInjection =
|
||||
isBedrockAppInferenceProfile(modelId) && !piAiWouldInjectCachePoints(modelId);
|
||||
const shouldOmitTemperature = isOpus47BedrockModelRef(modelId);
|
||||
const shouldPatchMaxThinking = shouldOmitTemperature && thinkingLevel === "max";
|
||||
|
||||
// For known Anthropic models (heuristic match), enable injection immediately.
|
||||
// For opaque profile IDs, we'll resolve via GetInferenceProfile on first call.
|
||||
const heuristicMatch = needsCachePointInjection(modelId);
|
||||
|
||||
if (!region && !mayNeedCacheInjection && !shouldOmitTemperature && !shouldPatchMaxThinking) {
|
||||
if (!region && !mayNeedCacheInjection && !shouldOmitTemperature) {
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
@@ -488,24 +447,8 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
Object.assign({}, options, region ? { region } : {}),
|
||||
);
|
||||
|
||||
const originalOnPayload = merged.onPayload as
|
||||
| ((payload: unknown, model: unknown) => unknown)
|
||||
| undefined;
|
||||
|
||||
if (!mayNeedCacheInjection) {
|
||||
return underlying(streamModel, context, {
|
||||
...merged,
|
||||
...(shouldPatchMaxThinking
|
||||
? {
|
||||
onPayload: (payload: unknown, payloadModel: unknown) => {
|
||||
if (payload && typeof payload === "object") {
|
||||
patchOpus47MaxThinkingEffort(payload as Record<string, unknown>);
|
||||
}
|
||||
return originalOnPayload?.(payload, payloadModel);
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
return underlying(streamModel, context, merged);
|
||||
}
|
||||
|
||||
// Use the cacheRetention from options if explicitly set.
|
||||
@@ -518,6 +461,10 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
// want caching enabled, so defaulting to "short" is the safer behavior.
|
||||
const cacheRetention =
|
||||
typeof merged.cacheRetention === "string" ? merged.cacheRetention : "short";
|
||||
const originalOnPayload = merged.onPayload as
|
||||
| ((payload: unknown, model: unknown) => unknown)
|
||||
| undefined;
|
||||
|
||||
if (heuristicMatch) {
|
||||
// Fast path: ARN heuristic already identified this as Claude, but the
|
||||
// concrete target may still need profile traits for Opus 4.7 payloads.
|
||||
@@ -528,9 +475,6 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
if (payload && typeof payload === "object") {
|
||||
const payloadRecord = payload as Record<string, unknown>;
|
||||
injectBedrockCachePoints(payloadRecord, cacheRetention);
|
||||
if (shouldPatchMaxThinking) {
|
||||
patchOpus47MaxThinkingEffort(payloadRecord);
|
||||
}
|
||||
if (mayNeedTemperatureTrait) {
|
||||
const traits = await resolveAppProfileTraits(modelId, region);
|
||||
if (traits.omitTemperature) {
|
||||
@@ -554,9 +498,6 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
if (traits.cacheEligible) {
|
||||
injectBedrockCachePoints(payloadRecord, cacheRetention);
|
||||
}
|
||||
if (shouldPatchMaxThinking) {
|
||||
patchOpus47MaxThinkingEffort(payloadRecord);
|
||||
}
|
||||
if (traits.omitTemperature) {
|
||||
omitDeprecatedOpus47PayloadTemperature(payloadRecord);
|
||||
}
|
||||
@@ -580,6 +521,16 @@ export function registerAmazonBedrockPlugin(api: OpenClawPluginApi): void {
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
resolveThinkingProfile: ({ modelId }) => resolveBedrockClaudeThinkingProfile(modelId),
|
||||
resolveThinkingProfile: ({ modelId }) => ({
|
||||
levels: [
|
||||
{ id: "off" },
|
||||
{ id: "minimal" },
|
||||
{ id: "low" },
|
||||
{ id: "medium" },
|
||||
{ id: "high" },
|
||||
...(claude46ModelRe.test(modelId.trim()) ? [{ id: "adaptive" as const }] : []),
|
||||
],
|
||||
defaultLevel: claude46ModelRe.test(modelId.trim()) ? "adaptive" : undefined,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -727,49 +727,6 @@ describe("gateway bonjour advertiser", () => {
|
||||
expect(shutdown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("disables bonjour when the advertiser flaps within a sliding window", async () => {
|
||||
enableAdvertiserUnitMode();
|
||||
vi.useFakeTimers();
|
||||
|
||||
const stateRef = { value: "announced" };
|
||||
const destroy = vi.fn().mockResolvedValue(undefined);
|
||||
const advertise = vi.fn().mockResolvedValue(undefined);
|
||||
mockCiaoService({ advertise, destroy, stateRef });
|
||||
|
||||
const started = await startAdvertiser({
|
||||
gatewayPort: 18789,
|
||||
sshPort: 2222,
|
||||
});
|
||||
|
||||
for (let cycle = 0; cycle < 12; cycle += 1) {
|
||||
stateRef.value = "announced";
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
stateRef.value = "probing";
|
||||
await vi.advanceTimersByTimeAsync(25_000);
|
||||
if (
|
||||
logger.warn.mock.calls.some(
|
||||
(call) => typeof call[0] === "string" && call[0].includes("disabling advertiser after"),
|
||||
)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const disableLog = logger.warn.mock.calls.find(
|
||||
(call) => typeof call[0] === "string" && call[0].includes("disabling advertiser after"),
|
||||
);
|
||||
expect(disableLog).toBeDefined();
|
||||
expect(String(disableLog?.[0])).toMatch(/restarts within \d+ minutes/);
|
||||
|
||||
const advertiseCallsAtDisable = advertise.mock.calls.length;
|
||||
const createServiceCallsAtDisable = createService.mock.calls.length;
|
||||
await vi.advanceTimersByTimeAsync(5 * 60_000);
|
||||
expect(advertise).toHaveBeenCalledTimes(advertiseCallsAtDisable);
|
||||
expect(createService).toHaveBeenCalledTimes(createServiceCallsAtDisable);
|
||||
|
||||
await started.stop();
|
||||
});
|
||||
|
||||
it("normalizes hostnames with domains for service names", async () => {
|
||||
// Allow advertiser to run in unit tests.
|
||||
delete process.env.VITEST;
|
||||
|
||||
@@ -88,10 +88,6 @@ const REPAIR_DEBOUNCE_MS = 30_000;
|
||||
// See https://github.com/openclaw/openclaw/issues/72481
|
||||
const STUCK_ANNOUNCING_MS = 20_000;
|
||||
const MAX_CONSECUTIVE_RESTARTS = 3;
|
||||
// A flapping advertiser can briefly reach "announced" between probing
|
||||
// failures, which resets the consecutive counter. Bound total restarts too.
|
||||
const RESTART_WINDOW_MS = 30 * 60_000;
|
||||
const MAX_RESTARTS_IN_WINDOW = 5;
|
||||
const BONJOUR_ANNOUNCED_STATE = "announced";
|
||||
const CIAO_SELF_PROBE_RETRY_FRAGMENT =
|
||||
"failed probing with reason: Error: Can't probe for a service which is announced already.";
|
||||
@@ -567,7 +563,6 @@ export async function startGatewayBonjourAdvertiser(
|
||||
let recreatePromise: Promise<void> | null = null;
|
||||
let disabled = false;
|
||||
let consecutiveRestarts = 0;
|
||||
const restartTimestamps: number[] = [];
|
||||
let cycle: BonjourCycle | null = createCycle();
|
||||
const stateTracker = new Map<string, ServiceStateTracker>();
|
||||
|
||||
@@ -595,25 +590,10 @@ export async function startGatewayBonjourAdvertiser(
|
||||
}
|
||||
recreatePromise = (async () => {
|
||||
consecutiveRestarts += 1;
|
||||
const now = Date.now();
|
||||
while (
|
||||
restartTimestamps.length > 0 &&
|
||||
now - (restartTimestamps[0] ?? 0) > RESTART_WINDOW_MS
|
||||
) {
|
||||
restartTimestamps.shift();
|
||||
}
|
||||
restartTimestamps.push(now);
|
||||
const tooManyConsecutive = consecutiveRestarts > MAX_CONSECUTIVE_RESTARTS;
|
||||
const tooManyInWindow = restartTimestamps.length >= MAX_RESTARTS_IN_WINDOW;
|
||||
if (tooManyConsecutive || tooManyInWindow) {
|
||||
if (consecutiveRestarts > MAX_CONSECUTIVE_RESTARTS) {
|
||||
disabled = true;
|
||||
const detail = tooManyConsecutive
|
||||
? `${MAX_CONSECUTIVE_RESTARTS} failed restarts`
|
||||
: `${MAX_RESTARTS_IN_WINDOW} restarts within ${Math.round(
|
||||
RESTART_WINDOW_MS / 60_000,
|
||||
)} minutes`;
|
||||
logger.warn(
|
||||
`bonjour: disabling advertiser after ${detail} (${reason}); set discovery.mdns.mode="off" or OPENCLAW_DISABLE_BONJOUR=1 to disable mDNS discovery`,
|
||||
`bonjour: disabling advertiser after ${MAX_CONSECUTIVE_RESTARTS} failed restarts (${reason}); set discovery.mdns.mode="off" or OPENCLAW_DISABLE_BONJOUR=1 to disable mDNS discovery`,
|
||||
);
|
||||
const previous = cycle;
|
||||
cycle = null;
|
||||
|
||||
@@ -3,20 +3,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const manageMocks = vi.hoisted(() => {
|
||||
const doctorAction = vi.fn();
|
||||
const openAction = vi.fn();
|
||||
const statusAction = vi.fn();
|
||||
const tabsAction = vi.fn();
|
||||
const registerBrowserManageCommands = vi.fn((browser: Command) => {
|
||||
browser.command("status").description("Show browser status").action(statusAction);
|
||||
browser.command("tabs").description("List tabs").action(tabsAction);
|
||||
browser.command("open").description("Open URL").argument("<url>").action(openAction);
|
||||
browser
|
||||
.command("doctor")
|
||||
.description("Check browser plugin readiness")
|
||||
.option("--deep", "Run a live snapshot probe")
|
||||
.action(doctorAction);
|
||||
});
|
||||
return { doctorAction, openAction, registerBrowserManageCommands, statusAction, tabsAction };
|
||||
return { doctorAction, registerBrowserManageCommands, statusAction };
|
||||
});
|
||||
const inspectMocks = vi.hoisted(() => ({
|
||||
registerBrowserInspectCommands: vi.fn(),
|
||||
@@ -48,9 +44,7 @@ describe("registerBrowserCli lazy browser subcommands", () => {
|
||||
vi.unstubAllEnvs();
|
||||
manageMocks.registerBrowserManageCommands.mockClear();
|
||||
manageMocks.doctorAction.mockClear();
|
||||
manageMocks.openAction.mockClear();
|
||||
manageMocks.statusAction.mockClear();
|
||||
manageMocks.tabsAction.mockClear();
|
||||
inspectMocks.registerBrowserInspectCommands.mockClear();
|
||||
actionInputMocks.registerBrowserActionInputCommands.mockClear();
|
||||
actionObserveMocks.registerBrowserActionObserveCommands.mockClear();
|
||||
@@ -109,29 +103,6 @@ describe("registerBrowserCli lazy browser subcommands", () => {
|
||||
expect(manageMocks.doctorAction.mock.calls[0]?.[0]).toMatchObject({ deep: true });
|
||||
});
|
||||
|
||||
it("preserves parent --json while reparsing lazy manage commands", async () => {
|
||||
const program = new Command();
|
||||
program.name("openclaw");
|
||||
|
||||
registerBrowserCli(program, ["node", "openclaw", "browser", "--json", "open", "about:blank"]);
|
||||
|
||||
await program.parseAsync(["browser", "--json", "open", "about:blank"], { from: "user" });
|
||||
|
||||
expect(manageMocks.openAction).toHaveBeenCalledTimes(1);
|
||||
const openCommand = manageMocks.openAction.mock.calls[0]?.at(-1) as Command | undefined;
|
||||
expect(openCommand?.parent?.opts()).toMatchObject({ json: true });
|
||||
|
||||
const tabsProgram = new Command();
|
||||
tabsProgram.name("openclaw");
|
||||
registerBrowserCli(tabsProgram, ["node", "openclaw", "browser", "--json", "tabs"]);
|
||||
|
||||
await tabsProgram.parseAsync(["browser", "--json", "tabs"], { from: "user" });
|
||||
|
||||
expect(manageMocks.tabsAction).toHaveBeenCalledTimes(1);
|
||||
const tabsCommand = manageMocks.tabsAction.mock.calls[0]?.at(-1) as Command | undefined;
|
||||
expect(tabsCommand?.parent?.opts()).toMatchObject({ json: true });
|
||||
});
|
||||
|
||||
it("can eagerly register all browser groups for compatibility", async () => {
|
||||
vi.stubEnv("OPENCLAW_DISABLE_LAZY_SUBCOMMANDS", "1");
|
||||
const program = new Command();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
ensureCodexComputerUse,
|
||||
installCodexComputerUse,
|
||||
@@ -12,8 +12,13 @@ import {
|
||||
describe("Codex Computer Use setup", () => {
|
||||
const cleanupPaths: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubEnv("OPENCLAW_MAC_NATIVE_HOST", "1");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllEnvs();
|
||||
for (const cleanupPath of cleanupPaths.splice(0)) {
|
||||
fs.rmSync(cleanupPath, { recursive: true, force: true });
|
||||
}
|
||||
@@ -32,6 +37,50 @@ describe("Codex Computer Use setup", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("requires the OpenClaw.app native host on macOS before contacting app-server", async () => {
|
||||
const request = vi.fn();
|
||||
|
||||
await expect(
|
||||
readCodexComputerUseStatus({
|
||||
pluginConfig: { computerUse: { enabled: true } },
|
||||
platform: "darwin",
|
||||
env: {},
|
||||
request,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
ready: false,
|
||||
reason: "native_host_missing",
|
||||
installed: false,
|
||||
pluginEnabled: false,
|
||||
mcpServerAvailable: false,
|
||||
message:
|
||||
"Computer Use on macOS requires OpenClaw.app to host the Gateway so macOS can grant desktop-control permissions to the native app. Open OpenClaw.app in Local mode, then run /codex computer-use install again.",
|
||||
}),
|
||||
);
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows explicit non-native macOS setup for diagnostics", async () => {
|
||||
const request = createComputerUseRequest({ installed: true });
|
||||
|
||||
await expect(
|
||||
readCodexComputerUseStatus({
|
||||
pluginConfig: { computerUse: { enabled: true, marketplaceName: "desktop-tools" } },
|
||||
platform: "darwin",
|
||||
env: { OPENCLAW_CODEX_COMPUTER_USE_ALLOW_NON_NATIVE_HOST: "1" },
|
||||
request,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
ready: true,
|
||||
reason: "ready",
|
||||
message: "Computer Use is ready.",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports an installed Computer Use MCP server from a registered marketplace", async () => {
|
||||
const request = createComputerUseRequest({ installed: true });
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export type CodexComputerUseRequest = <T = JsonValue | undefined>(
|
||||
|
||||
export type CodexComputerUseStatusReason =
|
||||
| "disabled"
|
||||
| "native_host_missing"
|
||||
| "marketplace_missing"
|
||||
| "plugin_not_installed"
|
||||
| "plugin_disabled"
|
||||
@@ -61,6 +62,8 @@ export type CodexComputerUseSetupParams = {
|
||||
signal?: AbortSignal;
|
||||
forceEnable?: boolean;
|
||||
defaultBundledMarketplacePath?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
platform?: NodeJS.Platform;
|
||||
};
|
||||
|
||||
type MarketplaceRef =
|
||||
@@ -132,6 +135,9 @@ export async function ensureCodexComputerUse(
|
||||
if (status.ready) {
|
||||
return status;
|
||||
}
|
||||
if (status.reason === "native_host_missing") {
|
||||
throw new CodexComputerUseSetupError(status);
|
||||
}
|
||||
if (config.autoInstall) {
|
||||
const blockedAutoInstallStatus = blockUnsafeAutoInstallStatus(config);
|
||||
if (blockedAutoInstallStatus) {
|
||||
@@ -181,7 +187,18 @@ async function inspectCodexComputerUse(params: {
|
||||
config: ResolvedCodexComputerUseConfig;
|
||||
installPlugin: boolean;
|
||||
defaultBundledMarketplacePath?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
platform?: NodeJS.Platform;
|
||||
}): Promise<CodexComputerUseStatus> {
|
||||
const nativeHostStatus = macNativeHostUnavailableStatus({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
platform: params.platform,
|
||||
});
|
||||
if (nativeHostStatus) {
|
||||
return nativeHostStatus;
|
||||
}
|
||||
|
||||
const request = createComputerUseRequest(params);
|
||||
if (params.installPlugin) {
|
||||
await request<v2.ExperimentalFeatureEnablementSetResponse>(
|
||||
@@ -417,6 +434,37 @@ function blockUnsafeAutoInstallStatus(
|
||||
);
|
||||
}
|
||||
|
||||
function macNativeHostUnavailableStatus(params: {
|
||||
config: ResolvedCodexComputerUseConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
platform?: NodeJS.Platform;
|
||||
}): CodexComputerUseStatus | undefined {
|
||||
const platform = params.platform ?? process.platform;
|
||||
if (platform !== "darwin") {
|
||||
return undefined;
|
||||
}
|
||||
const env = params.env ?? process.env;
|
||||
if (isTruthyEnvFlag(env.OPENCLAW_MAC_NATIVE_HOST)) {
|
||||
return undefined;
|
||||
}
|
||||
if (isTruthyEnvFlag(env.OPENCLAW_CODEX_COMPUTER_USE_ALLOW_NON_NATIVE_HOST)) {
|
||||
return undefined;
|
||||
}
|
||||
return unavailableStatus(
|
||||
params.config,
|
||||
"native_host_missing",
|
||||
"Computer Use on macOS requires OpenClaw.app to host the Gateway so macOS can grant desktop-control permissions to the native app. Open OpenClaw.app in Local mode, then run /codex computer-use install again.",
|
||||
);
|
||||
}
|
||||
|
||||
function isTruthyEnvFlag(value: string | undefined): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized !== "" && !["0", "false", "no", "off"].includes(normalized);
|
||||
}
|
||||
|
||||
function shouldAddBundledComputerUseMarketplace(params: {
|
||||
config: ResolvedCodexComputerUseConfig;
|
||||
allowAdd: boolean;
|
||||
|
||||
@@ -1056,106 +1056,6 @@ describe("runCodexAppServerAttempt", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("batches default queued steering before sending turn/steer", async () => {
|
||||
const { requests, waitForMethod, completeTurn } = createStartedThreadHarness();
|
||||
|
||||
const run = runCodexAppServerAttempt(
|
||||
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
|
||||
);
|
||||
await waitForMethod("turn/start");
|
||||
|
||||
expect(queueAgentHarnessMessage("session-1", "first", { debounceMs: 5 })).toBe(true);
|
||||
expect(queueAgentHarnessMessage("session-1", "second", { debounceMs: 5 })).toBe(true);
|
||||
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(requests.filter((entry) => entry.method === "turn/steer")).toEqual([
|
||||
{
|
||||
method: "turn/steer",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
expectedTurnId: "turn-1",
|
||||
input: [
|
||||
{ type: "text", text: "first", text_elements: [] },
|
||||
{ type: "text", text: "second", text_elements: [] },
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
{ interval: 1 },
|
||||
);
|
||||
|
||||
await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
|
||||
await run;
|
||||
});
|
||||
|
||||
it("flushes pending default queued steering during normal turn cleanup", async () => {
|
||||
const { requests, waitForMethod, completeTurn } = createStartedThreadHarness();
|
||||
|
||||
const run = runCodexAppServerAttempt(
|
||||
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
|
||||
);
|
||||
await waitForMethod("turn/start");
|
||||
|
||||
expect(queueAgentHarnessMessage("session-1", "late steer", { debounceMs: 30_000 })).toBe(true);
|
||||
|
||||
await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
|
||||
await run;
|
||||
|
||||
expect(requests.filter((entry) => entry.method === "turn/steer")).toEqual([
|
||||
{
|
||||
method: "turn/steer",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
expectedTurnId: "turn-1",
|
||||
input: [{ type: "text", text: "late steer", text_elements: [] }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps legacy queue steering as separate turn/steer requests", async () => {
|
||||
const { requests, waitForMethod, completeTurn } = createStartedThreadHarness();
|
||||
|
||||
const run = runCodexAppServerAttempt(
|
||||
createParams(path.join(tempDir, "session.jsonl"), path.join(tempDir, "workspace")),
|
||||
);
|
||||
await waitForMethod("turn/start");
|
||||
|
||||
expect(queueAgentHarnessMessage("session-1", "first", { steeringMode: "one-at-a-time" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(queueAgentHarnessMessage("session-1", "second", { steeringMode: "one-at-a-time" })).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await vi.waitFor(
|
||||
() =>
|
||||
expect(requests.filter((entry) => entry.method === "turn/steer")).toEqual([
|
||||
{
|
||||
method: "turn/steer",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
expectedTurnId: "turn-1",
|
||||
input: [{ type: "text", text: "first", text_elements: [] }],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "turn/steer",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
expectedTurnId: "turn-1",
|
||||
input: [{ type: "text", text: "second", text_elements: [] }],
|
||||
},
|
||||
},
|
||||
]),
|
||||
{ interval: 1 },
|
||||
);
|
||||
|
||||
await completeTurn({ threadId: "thread-1", turnId: "turn-1" });
|
||||
await run;
|
||||
});
|
||||
|
||||
it("routes MCP approval elicitations through the native bridge", async () => {
|
||||
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
|
||||
let handleRequest:
|
||||
|
||||
@@ -59,7 +59,6 @@ import {
|
||||
readCodexDynamicToolCallParams,
|
||||
} from "./protocol-validators.js";
|
||||
import {
|
||||
type CodexUserInput,
|
||||
isJsonObject,
|
||||
type CodexServerNotification,
|
||||
type CodexDynamicToolCallParams,
|
||||
@@ -87,7 +86,6 @@ import { filterToolsForVisionInputs } from "./vision-tools.js";
|
||||
|
||||
const CODEX_DYNAMIC_TOOL_TIMEOUT_MS = 30_000;
|
||||
const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 60_000;
|
||||
const CODEX_STEER_ALL_DEBOUNCE_MS = 500;
|
||||
|
||||
type OpenClawCodingToolsOptions = NonNullable<
|
||||
Parameters<(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"]>[0]
|
||||
@@ -125,94 +123,6 @@ function collectTerminalAssistantText(result: EmbeddedRunAttemptResult): string
|
||||
return result.assistantTexts.join("\n\n").trim();
|
||||
}
|
||||
|
||||
type CodexSteeringQueueOptions = {
|
||||
steeringMode?: "all" | "one-at-a-time";
|
||||
debounceMs?: number;
|
||||
};
|
||||
|
||||
function createCodexSteeringQueue(params: {
|
||||
client: CodexAppServerClient;
|
||||
threadId: string;
|
||||
turnId: string;
|
||||
answerPendingUserInput: (text: string) => boolean;
|
||||
signal: AbortSignal;
|
||||
}) {
|
||||
let batchedTexts: string[] = [];
|
||||
let batchTimer: NodeJS.Timeout | undefined;
|
||||
let sendChain: Promise<void> = Promise.resolve();
|
||||
|
||||
const clearBatchTimer = () => {
|
||||
if (batchTimer) {
|
||||
clearTimeout(batchTimer);
|
||||
batchTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const sendTexts = async (texts: string[]) => {
|
||||
if (texts.length === 0 || params.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
await params.client.request("turn/steer", {
|
||||
threadId: params.threadId,
|
||||
expectedTurnId: params.turnId,
|
||||
input: texts.map(toCodexTextInput),
|
||||
});
|
||||
};
|
||||
|
||||
const enqueueSend = (texts: string[]) => {
|
||||
sendChain = sendChain
|
||||
.then(() => sendTexts(texts))
|
||||
.catch((error: unknown) => {
|
||||
embeddedAgentLog.debug("codex app-server queued steer failed", { error });
|
||||
});
|
||||
return sendChain;
|
||||
};
|
||||
|
||||
const flushBatch = () => {
|
||||
clearBatchTimer();
|
||||
const texts = batchedTexts;
|
||||
batchedTexts = [];
|
||||
return enqueueSend(texts);
|
||||
};
|
||||
|
||||
return {
|
||||
async queue(text: string, options?: CodexSteeringQueueOptions) {
|
||||
if (params.answerPendingUserInput(text)) {
|
||||
return;
|
||||
}
|
||||
if (options?.steeringMode === "one-at-a-time") {
|
||||
await flushBatch();
|
||||
await enqueueSend([text]);
|
||||
return;
|
||||
}
|
||||
batchedTexts.push(text);
|
||||
clearBatchTimer();
|
||||
const debounceMs = normalizeCodexSteerDebounceMs(options?.debounceMs);
|
||||
batchTimer = setTimeout(() => {
|
||||
batchTimer = undefined;
|
||||
void flushBatch();
|
||||
}, debounceMs);
|
||||
},
|
||||
async flushPending() {
|
||||
await flushBatch();
|
||||
},
|
||||
cancel() {
|
||||
clearBatchTimer();
|
||||
batchedTexts = [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCodexSteerDebounceMs(value: number | undefined): number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
||||
? Math.floor(value)
|
||||
: CODEX_STEER_ALL_DEBOUNCE_MS;
|
||||
}
|
||||
|
||||
function toCodexTextInput(text: string): CodexUserInput {
|
||||
return { type: "text", text, text_elements: [] };
|
||||
}
|
||||
|
||||
export async function runCodexAppServerAttempt(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
options: {
|
||||
@@ -456,7 +366,6 @@ export async function runCodexAppServerAttempt(
|
||||
let turnId: string | undefined;
|
||||
const pendingNotifications: CodexServerNotification[] = [];
|
||||
let userInputBridge: ReturnType<typeof createCodexUserInputBridge> | undefined;
|
||||
let steeringQueue: ReturnType<typeof createCodexSteeringQueue> | undefined;
|
||||
let completed = false;
|
||||
let timedOut = false;
|
||||
let turnCompletionIdleTimedOut = false;
|
||||
@@ -590,9 +499,6 @@ export async function runCodexAppServerAttempt(
|
||||
});
|
||||
} finally {
|
||||
if (isTurnCompletion) {
|
||||
if (!timedOut && !runAbortController.signal.aborted) {
|
||||
await steeringQueue?.flushPending();
|
||||
}
|
||||
completed = true;
|
||||
clearTurnCompletionIdleTimer();
|
||||
resolveCompletion?.();
|
||||
@@ -821,18 +727,18 @@ export async function runCodexAppServerAttempt(
|
||||
});
|
||||
}
|
||||
|
||||
const activeSteeringQueue = createCodexSteeringQueue({
|
||||
client,
|
||||
threadId: thread.threadId,
|
||||
turnId: activeTurnId,
|
||||
answerPendingUserInput: (text) => userInputBridge?.handleQueuedMessage(text) ?? false,
|
||||
signal: runAbortController.signal,
|
||||
});
|
||||
steeringQueue = activeSteeringQueue;
|
||||
const handle = {
|
||||
kind: "embedded" as const,
|
||||
queueMessage: async (text: string, options?: CodexSteeringQueueOptions) =>
|
||||
activeSteeringQueue.queue(text, options),
|
||||
queueMessage: async (text: string) => {
|
||||
if (userInputBridge?.handleQueuedMessage(text)) {
|
||||
return;
|
||||
}
|
||||
await client.request("turn/steer", {
|
||||
threadId: thread.threadId,
|
||||
expectedTurnId: activeTurnId,
|
||||
input: [{ type: "text", text, text_elements: [] }],
|
||||
});
|
||||
},
|
||||
isStreaming: () => !completed,
|
||||
isCompacting: () => projector?.isCompacting() ?? false,
|
||||
cancel: () => runAbortController.abort("cancelled"),
|
||||
@@ -999,9 +905,6 @@ export async function runCodexAppServerAttempt(
|
||||
await trajectoryRecorder?.flush();
|
||||
},
|
||||
});
|
||||
if (!timedOut && !runAbortController.signal.aborted) {
|
||||
await steeringQueue?.flushPending();
|
||||
}
|
||||
userInputBridge?.cancelPending();
|
||||
clearTimeout(timeout);
|
||||
clearTurnCompletionIdleTimer();
|
||||
@@ -1010,7 +913,6 @@ export async function runCodexAppServerAttempt(
|
||||
nativeHookRelay?.unregister();
|
||||
runAbortController.signal.removeEventListener("abort", abortListener);
|
||||
params.abortSignal?.removeEventListener("abort", abortFromUpstream);
|
||||
steeringQueue?.cancel();
|
||||
clearActiveEmbeddedRun(params.sessionId, handle, params.sessionKey);
|
||||
}
|
||||
}
|
||||
@@ -1214,9 +1116,7 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
: undefined,
|
||||
modelApi: params.model.api,
|
||||
modelContextWindowTokens: params.model.contextWindow,
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
|
||||
workspaceDir: input.effectiveWorkspace,
|
||||
}),
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
currentMessageId: params.currentMessageId,
|
||||
|
||||
@@ -91,6 +91,13 @@ export function formatAccount(
|
||||
}
|
||||
|
||||
export function formatComputerUseStatus(status: CodexComputerUseStatus): string {
|
||||
if (status.reason === "native_host_missing") {
|
||||
return [
|
||||
"Computer Use: native host required",
|
||||
`Gateway host: OpenClaw.app required on macOS`,
|
||||
status.message,
|
||||
].join("\n");
|
||||
}
|
||||
const lines = [
|
||||
`Computer Use: ${status.ready ? "ready" : status.enabled ? "not ready" : "disabled"}`,
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { withFetchPreconnect } from "openclaw/plugin-sdk/test-env";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { DiscordApiError, fetchDiscord } from "./api.js";
|
||||
import { fetchDiscord } from "./api.js";
|
||||
import { jsonResponse } from "./test-http-helpers.js";
|
||||
|
||||
describe("fetchDiscord", () => {
|
||||
@@ -46,60 +46,6 @@ describe("fetchDiscord", () => {
|
||||
).rejects.toThrow("Discord API /users/@me/guilds failed (404): Not Found");
|
||||
});
|
||||
|
||||
it("sanitizes Cloudflare HTML rate limits and applies a fallback cooldown", async () => {
|
||||
const fetcher = withFetchPreconnect(
|
||||
async () =>
|
||||
new Response(
|
||||
"<!doctype html><html><head><title>Error 1015</title></head><body><h1>You are being rate limited</h1><script>raw()</script></body></html>",
|
||||
{ status: 429, headers: { "content-type": "text/html" } },
|
||||
),
|
||||
);
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await fetchDiscord("/users/@me/guilds", "test", fetcher, {
|
||||
retry: { attempts: 1 },
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(DiscordApiError);
|
||||
expect((error as DiscordApiError).retryAfter).toBe(60);
|
||||
const message = String(error);
|
||||
expect(message).toContain("Discord API /users/@me/guilds failed (429)");
|
||||
expect(message).toContain("rate limited by Discord upstream");
|
||||
expect(message).toContain("Error 1015");
|
||||
expect(message).not.toContain("<html");
|
||||
expect(message).not.toContain("<script");
|
||||
});
|
||||
|
||||
it("honors Retry-After for Cloudflare HTML application lookup rate limits", async () => {
|
||||
const fetcher = withFetchPreconnect(
|
||||
async () =>
|
||||
new Response("<html><title>Error 1015</title><body>rate limited</body></html>", {
|
||||
status: 429,
|
||||
headers: { "content-type": "text/html", "retry-after": "7" },
|
||||
}),
|
||||
);
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await fetchDiscord("/oauth2/applications/@me", "test", fetcher, {
|
||||
retry: { attempts: 1 },
|
||||
});
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(DiscordApiError);
|
||||
expect((error as DiscordApiError).retryAfter).toBe(7);
|
||||
const message = String(error);
|
||||
expect(message).toContain("Discord API /oauth2/applications/@me failed (429)");
|
||||
expect(message).toContain("Error 1015");
|
||||
expect(message).not.toContain("<html");
|
||||
});
|
||||
|
||||
it("retries rate limits before succeeding", async () => {
|
||||
let calls = 0;
|
||||
const fetcher = withFetchPreconnect(async () => {
|
||||
|
||||
@@ -4,16 +4,14 @@ import {
|
||||
retryAsync,
|
||||
type RetryConfig,
|
||||
} from "openclaw/plugin-sdk/retry-runtime";
|
||||
import { isDiscordHtmlResponseBody, summarizeDiscordResponseBody } from "./error-body.js";
|
||||
|
||||
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
||||
const DISCORD_API_RETRY_DEFAULTS = {
|
||||
attempts: 3,
|
||||
minDelayMs: 500,
|
||||
maxDelayMs: 5 * 60_000,
|
||||
maxDelayMs: 30_000,
|
||||
jitter: 0.1,
|
||||
};
|
||||
const DISCORD_API_429_FALLBACK_RETRY_AFTER_SECONDS = 60;
|
||||
|
||||
type DiscordApiErrorPayload = {
|
||||
message?: string;
|
||||
@@ -52,14 +50,7 @@ function parseRetryAfterSeconds(text: string, response: Response): number | unde
|
||||
return undefined;
|
||||
}
|
||||
const parsed = Number(header);
|
||||
if (Number.isFinite(parsed) && parsed >= 0) {
|
||||
return parsed;
|
||||
}
|
||||
const retryAt = Date.parse(header);
|
||||
if (!Number.isFinite(retryAt)) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.max(0, (retryAt - Date.now()) / 1000);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function formatRetryAfterSeconds(value: number | undefined): string | undefined {
|
||||
@@ -70,7 +61,7 @@ function formatRetryAfterSeconds(value: number | undefined): string | undefined
|
||||
return `${rounded}s`;
|
||||
}
|
||||
|
||||
function formatDiscordApiErrorText(text: string, response: Response): string | undefined {
|
||||
function formatDiscordApiErrorText(text: string): string | undefined {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
@@ -78,17 +69,7 @@ function formatDiscordApiErrorText(text: string, response: Response): string | u
|
||||
const payload = parseDiscordApiErrorPayload(trimmed);
|
||||
if (!payload) {
|
||||
const looksJson = trimmed.startsWith("{") && trimmed.endsWith("}");
|
||||
if (looksJson) {
|
||||
return "unknown error";
|
||||
}
|
||||
const summary = summarizeDiscordResponseBody(trimmed);
|
||||
if (isDiscordHtmlResponseBody(trimmed, response.headers.get("content-type"))) {
|
||||
if (!summary) {
|
||||
return response.status === 429 ? "rate limited by Discord upstream" : undefined;
|
||||
}
|
||||
return response.status === 429 ? `rate limited by Discord upstream: ${summary}` : summary;
|
||||
}
|
||||
return summary;
|
||||
return looksJson ? "unknown error" : trimmed;
|
||||
}
|
||||
const message =
|
||||
typeof payload.message === "string" && payload.message.trim()
|
||||
@@ -111,16 +92,6 @@ export class DiscordApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function getDiscordApiRetryAfterMs(
|
||||
err: unknown,
|
||||
retryConfig: Required<RetryConfig>,
|
||||
): number | undefined {
|
||||
if (!(err instanceof DiscordApiError) || typeof err.retryAfter !== "number") {
|
||||
return undefined;
|
||||
}
|
||||
return Math.min(Math.max(0, err.retryAfter * 1000), retryConfig.maxDelayMs);
|
||||
}
|
||||
|
||||
export type DiscordFetchOptions = {
|
||||
retry?: RetryConfig;
|
||||
label?: string;
|
||||
@@ -145,12 +116,9 @@ export async function fetchDiscord<T>(
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => "");
|
||||
const detail = formatDiscordApiErrorText(text, res);
|
||||
const detail = formatDiscordApiErrorText(text);
|
||||
const suffix = detail ? `: ${detail}` : "";
|
||||
const retryAfter =
|
||||
res.status === 429
|
||||
? (parseRetryAfterSeconds(text, res) ?? DISCORD_API_429_FALLBACK_RETRY_AFTER_SECONDS)
|
||||
: undefined;
|
||||
const retryAfter = res.status === 429 ? parseRetryAfterSeconds(text, res) : undefined;
|
||||
throw new DiscordApiError(
|
||||
`Discord API ${path} failed (${res.status})${suffix}`,
|
||||
res.status,
|
||||
@@ -163,7 +131,10 @@ export async function fetchDiscord<T>(
|
||||
...retryConfig,
|
||||
label: options?.label ?? path,
|
||||
shouldRetry: (err) => err instanceof DiscordApiError && err.status === 429,
|
||||
retryAfterMs: (err) => getDiscordApiRetryAfterMs(err, retryConfig),
|
||||
retryAfterMs: (err) =>
|
||||
err instanceof DiscordApiError && typeof err.retryAfter === "number"
|
||||
? err.retryAfter * 1000
|
||||
: undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,30 +76,6 @@ describe("discord config schema", () => {
|
||||
expect(cfg.historyLimit).toBe(3);
|
||||
});
|
||||
|
||||
it("accepts Discord application IDs at top-level and account scope", () => {
|
||||
const cfg = expectValidDiscordConfig({
|
||||
applicationId: "123456789012345678",
|
||||
accounts: {
|
||||
work: {
|
||||
applicationId: 234567890123456,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(cfg.applicationId).toBe("123456789012345678");
|
||||
expect(cfg.accounts?.work?.applicationId).toBe("234567890123456");
|
||||
});
|
||||
|
||||
it("rejects unsafe numeric Discord application IDs", () => {
|
||||
const issues = expectInvalidDiscordConfig({
|
||||
applicationId: 106232522769186816,
|
||||
});
|
||||
|
||||
expect(
|
||||
issues.some((issue) => issue.message.includes("not a valid non-negative safe integer")),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("loads guild map and dm group settings", () => {
|
||||
const cfg = expectValidDiscordConfig({
|
||||
enabled: true,
|
||||
|
||||
@@ -222,8 +222,4 @@ export const discordChannelConfigUiHints = {
|
||||
help: "Discord bot token used for gateway and REST API authentication for this provider account. Keep this secret out of committed config and rotate immediately after any leak.",
|
||||
sensitive: true,
|
||||
},
|
||||
applicationId: {
|
||||
label: "Discord Application ID",
|
||||
help: "Optional Discord application/client ID. Set this when hosted environments cannot reach Discord's application lookup endpoint during startup.",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
const DISCORD_RESPONSE_BODY_SUMMARY_MAX_CHARS = 240;
|
||||
|
||||
export function summarizeDiscordResponseBody(
|
||||
body: string,
|
||||
opts: { emptyText?: string } = {},
|
||||
): string | undefined {
|
||||
const summary = body
|
||||
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, " ")
|
||||
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, " ")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/ /gi, " ")
|
||||
.replace(/&/gi, "&")
|
||||
.replace(/</gi, "<")
|
||||
.replace(/>/gi, ">")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (!summary) {
|
||||
return opts.emptyText;
|
||||
}
|
||||
return summary.slice(0, DISCORD_RESPONSE_BODY_SUMMARY_MAX_CHARS);
|
||||
}
|
||||
|
||||
export function isDiscordHtmlResponseBody(body: string, contentType?: string | null): boolean {
|
||||
return (
|
||||
/\bhtml\b/i.test(contentType ?? "") ||
|
||||
/^\s*<!doctype\s+html\b/i.test(body) ||
|
||||
/^\s*<html\b/i.test(body)
|
||||
);
|
||||
}
|
||||
|
||||
export function isDiscordRateLimitResponseBody(body: string): boolean {
|
||||
const normalized = body.toLowerCase();
|
||||
return (
|
||||
normalized.includes("error 1015") ||
|
||||
normalized.includes("cloudflare") ||
|
||||
normalized.includes("rate limit")
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
resolveEnvelopeFormatOptions,
|
||||
} from "openclaw/plugin-sdk/channel-inbound";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
import { runInboundReplyTurn } from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
||||
import { runPreparedInboundReplyTurn } from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
||||
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { createNonExitingRuntime, logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
@@ -270,97 +270,83 @@ export async function dispatchDiscordComponentEvent(params: {
|
||||
startId: params.replyToId,
|
||||
});
|
||||
|
||||
await runInboundReplyTurn({
|
||||
await runPreparedInboundReplyTurn({
|
||||
channel: "discord",
|
||||
accountId,
|
||||
raw: interaction,
|
||||
adapter: {
|
||||
ingest: () => ({
|
||||
id: interaction.id,
|
||||
rawText: ctxPayload.RawBody ?? "",
|
||||
textForAgent: ctxPayload.BodyForAgent,
|
||||
textForCommands: ctxPayload.CommandBody,
|
||||
raw: interaction,
|
||||
}),
|
||||
resolveTurn: () => ({
|
||||
channel: "discord",
|
||||
accountId,
|
||||
routeSessionKey: sessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession,
|
||||
record: {
|
||||
updateLastRoute: interactionCtx.isDirectMessage
|
||||
? {
|
||||
sessionKey: route.mainSessionKey,
|
||||
channel: "discord",
|
||||
to:
|
||||
resolveDiscordComponentOriginatingTo(interactionCtx) ??
|
||||
`user:${interactionCtx.userId}`,
|
||||
routeSessionKey: sessionKey,
|
||||
storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession,
|
||||
record: {
|
||||
updateLastRoute: interactionCtx.isDirectMessage
|
||||
? {
|
||||
sessionKey: route.mainSessionKey,
|
||||
channel: "discord",
|
||||
to:
|
||||
resolveDiscordComponentOriginatingTo(interactionCtx) ??
|
||||
`user:${interactionCtx.userId}`,
|
||||
accountId,
|
||||
mainDmOwnerPin: pinnedMainDmOwner
|
||||
? {
|
||||
ownerRecipient: pinnedMainDmOwner,
|
||||
senderRecipient: interactionCtx.userId,
|
||||
onSkip: ({ ownerRecipient, senderRecipient }) => {
|
||||
logVerbose(
|
||||
`discord: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
onRecordError: (err) => {
|
||||
logVerbose(`discord: failed updating component session meta: ${String(err)}`);
|
||||
},
|
||||
},
|
||||
runDispatch: () =>
|
||||
dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
cfg: ctx.cfg,
|
||||
replyOptions: { onModelSelected },
|
||||
dispatcherOptions: {
|
||||
...replyPipeline,
|
||||
humanDelay: resolveHumanDelayConfig(ctx.cfg, agentId),
|
||||
deliver: async (payload) => {
|
||||
const replyToId = replyReference.use();
|
||||
await deliverDiscordReply({
|
||||
cfg: ctx.cfg,
|
||||
replies: [payload],
|
||||
target: deliverTarget,
|
||||
token,
|
||||
accountId,
|
||||
rest: interaction.client.rest,
|
||||
runtime,
|
||||
replyToId,
|
||||
replyToMode,
|
||||
textLimit,
|
||||
maxLinesPerMessage: resolveDiscordMaxLinesPerMessage({
|
||||
cfg: ctx.cfg,
|
||||
discordConfig: ctx.discordConfig,
|
||||
accountId,
|
||||
mainDmOwnerPin: pinnedMainDmOwner
|
||||
? {
|
||||
ownerRecipient: pinnedMainDmOwner,
|
||||
senderRecipient: interactionCtx.userId,
|
||||
onSkip: ({ ownerRecipient, senderRecipient }) => {
|
||||
logVerbose(
|
||||
`discord: skip main-session last route for ${senderRecipient} (pinned owner ${ownerRecipient})`,
|
||||
);
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
onRecordError: (err) => {
|
||||
logVerbose(`discord: failed updating component session meta: ${String(err)}`);
|
||||
}),
|
||||
tableMode,
|
||||
chunkMode: resolveChunkMode(ctx.cfg, "discord", accountId),
|
||||
mediaLocalRoots,
|
||||
});
|
||||
replyReference.markSent();
|
||||
},
|
||||
onReplyStart: async () => {
|
||||
try {
|
||||
const { sendTyping } = await loadTypingRuntime();
|
||||
await sendTyping({ rest: feedbackRest, channelId: typingChannelId });
|
||||
} catch (err) {
|
||||
logVerbose(`discord: typing failed for component reply: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
logError(`discord component dispatch failed: ${String(err)}`);
|
||||
},
|
||||
},
|
||||
runDispatch: () =>
|
||||
dispatchReplyWithBufferedBlockDispatcher({
|
||||
ctx: ctxPayload,
|
||||
cfg: ctx.cfg,
|
||||
replyOptions: { onModelSelected },
|
||||
dispatcherOptions: {
|
||||
...replyPipeline,
|
||||
humanDelay: resolveHumanDelayConfig(ctx.cfg, agentId),
|
||||
deliver: async (payload) => {
|
||||
const replyToId = replyReference.use();
|
||||
await deliverDiscordReply({
|
||||
cfg: ctx.cfg,
|
||||
replies: [payload],
|
||||
target: deliverTarget,
|
||||
token,
|
||||
accountId,
|
||||
rest: interaction.client.rest,
|
||||
runtime,
|
||||
replyToId,
|
||||
replyToMode,
|
||||
textLimit,
|
||||
maxLinesPerMessage: resolveDiscordMaxLinesPerMessage({
|
||||
cfg: ctx.cfg,
|
||||
discordConfig: ctx.discordConfig,
|
||||
accountId,
|
||||
}),
|
||||
tableMode,
|
||||
chunkMode: resolveChunkMode(ctx.cfg, "discord", accountId),
|
||||
mediaLocalRoots,
|
||||
});
|
||||
replyReference.markSent();
|
||||
},
|
||||
onReplyStart: async () => {
|
||||
try {
|
||||
const { sendTyping } = await loadTypingRuntime();
|
||||
await sendTyping({ rest: feedbackRest, channelId: typingChannelId });
|
||||
} catch (err) {
|
||||
logVerbose(`discord: typing failed for component reply: ${String(err)}`);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
logError(`discord component dispatch failed: ${String(err)}`);
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fetchDiscordGatewayInfo, resolveGatewayInfoWithFallback } from "./gateway-metadata.js";
|
||||
|
||||
describe("Discord gateway metadata", () => {
|
||||
it("falls back on Cloudflare HTML rate limits without logging raw HTML", async () => {
|
||||
const error = await fetchDiscordGatewayInfo({
|
||||
token: "test",
|
||||
fetchImpl: async () =>
|
||||
new Response("<html><title>Error 1015</title><body>rate limited</body></html>", {
|
||||
status: 429,
|
||||
headers: { "content-type": "text/html" },
|
||||
}),
|
||||
}).catch((err: unknown) => err);
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
const resolved = resolveGatewayInfoWithFallback({ runtime, error });
|
||||
|
||||
expect(resolved.usedFallback).toBe(true);
|
||||
expect(resolved.info.url).toBe("wss://gateway.discord.gg/");
|
||||
const logs = runtime.log.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(logs).toContain("gateway metadata lookup failed transiently");
|
||||
expect(logs).toContain("Error 1015");
|
||||
expect(logs).not.toContain("<html");
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,9 @@ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { captureHttpExchange } from "openclaw/plugin-sdk/proxy-capture";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { Type } from "typebox";
|
||||
import { Check, Errors } from "typebox/value";
|
||||
import { isDiscordRateLimitResponseBody, summarizeDiscordResponseBody } from "../error-body.js";
|
||||
import { withAbortTimeout } from "./timeouts.js";
|
||||
|
||||
const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot";
|
||||
@@ -80,21 +80,18 @@ export function resolveDiscordGatewayInfoTimeoutMs(params?: {
|
||||
}
|
||||
|
||||
function summarizeGatewayResponseBody(body: string): string {
|
||||
return summarizeDiscordResponseBody(body, { emptyText: "<empty>" }) ?? "<empty>";
|
||||
}
|
||||
|
||||
function isDiscordGatewayRateLimitResponse(status: number, body: string): boolean {
|
||||
return status === 429 && isDiscordRateLimitResponseBody(body);
|
||||
const normalized = body.trim().replace(/\s+/g, " ");
|
||||
if (!normalized) {
|
||||
return "<empty>";
|
||||
}
|
||||
return normalized.slice(0, 240);
|
||||
}
|
||||
|
||||
function isTransientDiscordGatewayResponse(status: number, body: string): boolean {
|
||||
if (status >= 500) {
|
||||
return true;
|
||||
}
|
||||
if (isDiscordGatewayRateLimitResponse(status, body)) {
|
||||
return true;
|
||||
}
|
||||
const normalized = body.toLowerCase();
|
||||
const normalized = normalizeLowercaseStringOrEmpty(body);
|
||||
return (
|
||||
normalized.includes("upstream connect error") ||
|
||||
normalized.includes("disconnect/reset before headers") ||
|
||||
|
||||
@@ -13,14 +13,12 @@ import {
|
||||
} from "openclaw/plugin-sdk/channel-reply-pipeline";
|
||||
import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-streaming";
|
||||
import { recordInboundSession } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import {
|
||||
hasFinalInboundReplyDispatch,
|
||||
runInboundReplyTurn,
|
||||
} from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
||||
import { runPreparedInboundReplyTurn } from "openclaw/plugin-sdk/inbound-reply-dispatch";
|
||||
import { resolveMarkdownTableMode } from "openclaw/plugin-sdk/markdown-table-runtime";
|
||||
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { resolveChunkMode } from "openclaw/plugin-sdk/reply-chunking";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-dispatch-runtime";
|
||||
import { clearHistoryEntriesIfEnabled } from "openclaw/plugin-sdk/reply-history";
|
||||
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { danger, logVerbose, shouldLogVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { resolveDiscordMaxLinesPerMessage } from "../accounts.js";
|
||||
@@ -479,135 +477,109 @@ export async function processDiscordMessage(
|
||||
await settleDispatchBeforeStart();
|
||||
return;
|
||||
}
|
||||
const preparedResult = await runInboundReplyTurn({
|
||||
const preparedResult = await runPreparedInboundReplyTurn({
|
||||
channel: "discord",
|
||||
accountId: route.accountId,
|
||||
raw: ctx,
|
||||
adapter: {
|
||||
ingest: () => ({
|
||||
id: message.id,
|
||||
timestamp: message.timestamp ? Date.parse(message.timestamp) : undefined,
|
||||
rawText: text,
|
||||
textForAgent: ctxPayload.BodyForAgent,
|
||||
textForCommands: ctxPayload.CommandBody,
|
||||
raw: message,
|
||||
}),
|
||||
resolveTurn: () => ({
|
||||
channel: "discord",
|
||||
accountId: route.accountId,
|
||||
routeSessionKey: persistedSessionKey,
|
||||
storePath: turn.storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession,
|
||||
record: turn.record,
|
||||
history: {
|
||||
isGroup: isGuildMessage,
|
||||
historyKey: messageChannelId,
|
||||
historyMap: guildHistories,
|
||||
limit: historyLimit,
|
||||
routeSessionKey: persistedSessionKey,
|
||||
storePath: turn.storePath,
|
||||
ctxPayload,
|
||||
recordInboundSession,
|
||||
record: turn.record,
|
||||
onPreDispatchFailure: settleDispatchBeforeStart,
|
||||
runDispatch: () =>
|
||||
dispatchInboundMessage({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions: {
|
||||
...replyOptions,
|
||||
abortSignal,
|
||||
skillFilter: channelConfig?.skills,
|
||||
sourceReplyDeliveryMode,
|
||||
disableBlockStreaming: sourceRepliesAreToolOnly
|
||||
? true
|
||||
: (draftPreview.disableBlockStreamingForDraft ??
|
||||
(typeof resolvedBlockStreamingEnabled === "boolean"
|
||||
? !resolvedBlockStreamingEnabled
|
||||
: undefined)),
|
||||
onPartialReply: draftPreview.draftStream
|
||||
? (payload) => draftPreview.updateFromPartial(payload.text)
|
||||
: undefined,
|
||||
onAssistantMessageStart: draftPreview.draftStream
|
||||
? draftPreview.handleAssistantMessageBoundary
|
||||
: undefined,
|
||||
onReasoningEnd: draftPreview.draftStream
|
||||
? draftPreview.handleAssistantMessageBoundary
|
||||
: undefined,
|
||||
onModelSelected,
|
||||
suppressDefaultToolProgressMessages: draftPreview.previewToolProgressEnabled
|
||||
? true
|
||||
: undefined,
|
||||
onReasoningStream: async () => {
|
||||
await statusReactions.setThinking();
|
||||
},
|
||||
onToolStart: async (payload) => {
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
await statusReactions.setTool(payload.name);
|
||||
draftPreview.pushToolProgress(
|
||||
payload.name ? `tool: ${payload.name}` : "tool running",
|
||||
);
|
||||
},
|
||||
onItemEvent: async (payload) => {
|
||||
draftPreview.pushToolProgress(
|
||||
payload.progressText ?? payload.summary ?? payload.title ?? payload.name,
|
||||
);
|
||||
},
|
||||
onPlanUpdate: async (payload) => {
|
||||
if (payload.phase !== "update") {
|
||||
return;
|
||||
}
|
||||
draftPreview.pushToolProgress(
|
||||
payload.explanation ?? payload.steps?.[0] ?? "planning",
|
||||
);
|
||||
},
|
||||
onApprovalEvent: async (payload) => {
|
||||
if (payload.phase !== "requested") {
|
||||
return;
|
||||
}
|
||||
draftPreview.pushToolProgress(
|
||||
payload.command ? `approval: ${payload.command}` : "approval requested",
|
||||
);
|
||||
},
|
||||
onCommandOutput: async (payload) => {
|
||||
if (payload.phase !== "end") {
|
||||
return;
|
||||
}
|
||||
draftPreview.pushToolProgress(
|
||||
payload.name
|
||||
? `${payload.name}${payload.exitCode === 0 ? " ✓" : payload.exitCode != null ? ` (exit ${payload.exitCode})` : ""}`
|
||||
: payload.title,
|
||||
);
|
||||
},
|
||||
onPatchSummary: async (payload) => {
|
||||
if (payload.phase !== "end") {
|
||||
return;
|
||||
}
|
||||
draftPreview.pushToolProgress(payload.summary ?? payload.title ?? "patch applied");
|
||||
},
|
||||
onCompactionStart: async () => {
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
await statusReactions.setCompacting();
|
||||
},
|
||||
onCompactionEnd: async () => {
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
statusReactions.cancelPending();
|
||||
await statusReactions.setThinking();
|
||||
},
|
||||
},
|
||||
onPreDispatchFailure: settleDispatchBeforeStart,
|
||||
runDispatch: () =>
|
||||
dispatchInboundMessage({
|
||||
ctx: ctxPayload,
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyOptions: {
|
||||
...replyOptions,
|
||||
abortSignal,
|
||||
skillFilter: channelConfig?.skills,
|
||||
sourceReplyDeliveryMode,
|
||||
disableBlockStreaming: sourceRepliesAreToolOnly
|
||||
? true
|
||||
: (draftPreview.disableBlockStreamingForDraft ??
|
||||
(typeof resolvedBlockStreamingEnabled === "boolean"
|
||||
? !resolvedBlockStreamingEnabled
|
||||
: undefined)),
|
||||
onPartialReply: draftPreview.draftStream
|
||||
? (payload) => draftPreview.updateFromPartial(payload.text)
|
||||
: undefined,
|
||||
onAssistantMessageStart: draftPreview.draftStream
|
||||
? draftPreview.handleAssistantMessageBoundary
|
||||
: undefined,
|
||||
onReasoningEnd: draftPreview.draftStream
|
||||
? draftPreview.handleAssistantMessageBoundary
|
||||
: undefined,
|
||||
onModelSelected,
|
||||
suppressDefaultToolProgressMessages: draftPreview.previewToolProgressEnabled
|
||||
? true
|
||||
: undefined,
|
||||
onReasoningStream: async () => {
|
||||
await statusReactions.setThinking();
|
||||
},
|
||||
onToolStart: async (payload) => {
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
await statusReactions.setTool(payload.name);
|
||||
draftPreview.pushToolProgress(
|
||||
payload.name ? `tool: ${payload.name}` : "tool running",
|
||||
);
|
||||
},
|
||||
onItemEvent: async (payload) => {
|
||||
draftPreview.pushToolProgress(
|
||||
payload.progressText ?? payload.summary ?? payload.title ?? payload.name,
|
||||
);
|
||||
},
|
||||
onPlanUpdate: async (payload) => {
|
||||
if (payload.phase !== "update") {
|
||||
return;
|
||||
}
|
||||
draftPreview.pushToolProgress(
|
||||
payload.explanation ?? payload.steps?.[0] ?? "planning",
|
||||
);
|
||||
},
|
||||
onApprovalEvent: async (payload) => {
|
||||
if (payload.phase !== "requested") {
|
||||
return;
|
||||
}
|
||||
draftPreview.pushToolProgress(
|
||||
payload.command ? `approval: ${payload.command}` : "approval requested",
|
||||
);
|
||||
},
|
||||
onCommandOutput: async (payload) => {
|
||||
if (payload.phase !== "end") {
|
||||
return;
|
||||
}
|
||||
draftPreview.pushToolProgress(
|
||||
payload.name
|
||||
? `${payload.name}${payload.exitCode === 0 ? " ✓" : payload.exitCode != null ? ` (exit ${payload.exitCode})` : ""}`
|
||||
: payload.title,
|
||||
);
|
||||
},
|
||||
onPatchSummary: async (payload) => {
|
||||
if (payload.phase !== "end") {
|
||||
return;
|
||||
}
|
||||
draftPreview.pushToolProgress(
|
||||
payload.summary ?? payload.title ?? "patch applied",
|
||||
);
|
||||
},
|
||||
onCompactionStart: async () => {
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
await statusReactions.setCompacting();
|
||||
},
|
||||
onCompactionEnd: async () => {
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
return;
|
||||
}
|
||||
statusReactions.cancelPending();
|
||||
await statusReactions.setThinking();
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
if (!preparedResult.dispatched) {
|
||||
return;
|
||||
}
|
||||
dispatchResult = preparedResult.dispatchResult;
|
||||
if (isProcessAborted(abortSignal)) {
|
||||
dispatchAborted = true;
|
||||
@@ -671,14 +643,27 @@ export async function processDiscordMessage(
|
||||
return;
|
||||
}
|
||||
|
||||
const finalDispatchResult = dispatchResult;
|
||||
if (!finalDispatchResult || !hasFinalInboundReplyDispatch(finalDispatchResult)) {
|
||||
if (!dispatchResult?.queuedFinal) {
|
||||
if (isGuildMessage) {
|
||||
clearHistoryEntriesIfEnabled({
|
||||
historyMap: guildHistories,
|
||||
historyKey: messageChannelId,
|
||||
limit: historyLimit,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (shouldLogVerbose()) {
|
||||
const finalCount = finalDispatchResult.counts.final;
|
||||
const finalCount = dispatchResult.counts.final;
|
||||
logVerbose(
|
||||
`discord: delivered ${finalCount} reply${finalCount === 1 ? "" : "ies"} to ${replyTarget}`,
|
||||
);
|
||||
}
|
||||
if (isGuildMessage) {
|
||||
clearHistoryEntriesIfEnabled({
|
||||
historyMap: guildHistories,
|
||||
historyKey: messageChannelId,
|
||||
limit: historyLimit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -887,30 +887,6 @@ describe("monitorDiscordProvider", () => {
|
||||
expect(getConstructedClientOptions().clientId).toBe("123");
|
||||
});
|
||||
|
||||
it("uses configured application id before token parsing or REST lookup", async () => {
|
||||
const fetchApplicationId = vi.fn(async () => "network-app");
|
||||
providerTesting.setFetchDiscordApplicationId(fetchApplicationId);
|
||||
resolveDiscordAccountMock.mockReturnValue({
|
||||
accountId: "default",
|
||||
token: "MTIz.abc.def",
|
||||
config: {
|
||||
applicationId: "987654321098765432",
|
||||
commands: { native: true, nativeSkills: false },
|
||||
voice: { enabled: false },
|
||||
agentComponents: { enabled: false },
|
||||
execApprovals: { enabled: false },
|
||||
},
|
||||
});
|
||||
|
||||
await monitorDiscordProvider({
|
||||
config: baseConfig(),
|
||||
runtime: baseRuntime(),
|
||||
});
|
||||
|
||||
expect(fetchApplicationId).not.toHaveBeenCalled();
|
||||
expect(getConstructedClientOptions().clientId).toBe("987654321098765432");
|
||||
});
|
||||
|
||||
it("reports connected status on startup and shutdown", async () => {
|
||||
const setStatus = vi.fn();
|
||||
clientGetPluginMock.mockImplementation((name: string) =>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user