Compare commits

..

167 Commits

Author SHA1 Message Date
Vincent Koc
6d46cd809b fix(codex): omit tool controls without tools 2026-05-30 02:20:32 +02:00
Peter Steinberger
43658872d9 test: stabilize sandbox browser audit timers 2026-05-30 01:18:53 +01:00
Dallin Romney
bd04d2db0d feat: only include the current changelog section in tarball (#88107)
* build: package current changelog section

* build: guard packaged changelog section size
2026-05-29 17:18:35 -07:00
Merlin
c8a733eae5 fix(gateway): resolve message actions against runtime config (#84535)
* fix(gateway): resolve message action config from runtime snapshot

* fix(gateway): preserve runtime config matching through auto-enable

* fix(gateway): preserve auto-enabled message action fallback

* fix(gateway): use canonical runtime snapshot for message actions

* fix(discord): route credential actions through gateway

---------

Co-authored-by: Merlin <258679497+funmerlin@users.noreply.github.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-05-29 17:14:45 -07:00
Vincent Koc
3e35f599bc refactor: collapse zalo runtime api barrel 2026-05-30 02:11:24 +02:00
Dallin Romney
914f313740 test(unit-fast): isolate fake-timer files (#88160) 2026-05-29 17:11:05 -07:00
Peter Steinberger
4efc48a80d test(ci): stabilize sandbox browser audit timeout 2026-05-30 02:06:58 +02:00
Vincent Koc
ecc5601b2a fix(github): bound proof comment API bodies 2026-05-30 01:58:19 +02:00
Peter Steinberger
14795dc0cc test: stabilize block reply abort timers 2026-05-30 00:56:15 +01:00
Peter Steinberger
05dee6760d test: stabilize tool search fetch timeout 2026-05-30 00:54:20 +01:00
Peter Steinberger
582aa1ceb2 test(ci): stabilize tool search gateway timeout helper 2026-05-30 01:49:13 +02:00
Peter Steinberger
f6e1bc393b fix(fal): cap video queue deadline 2026-05-29 19:38:41 -04:00
Peter Steinberger
c91d1048e4 fix(release): harden release ci summary lookup 2026-05-30 00:35:57 +01:00
Peter Steinberger
90994a38a0 fix(openrouter): cap music stream timeout 2026-05-29 19:34:45 -04:00
Vincent Koc
c01a0f5588 refactor: share provider oauth runtime helpers 2026-05-30 01:31:10 +02:00
Peter Steinberger
8ff61be8d6 fix(providers): cap local service timers 2026-05-29 19:29:40 -04:00
Peter Steinberger
90d569e896 fix(telegram): centralize positive timer bounds 2026-05-29 19:25:30 -04:00
Peter Steinberger
d8bc71f222 test: stabilize realtime websocket timeout 2026-05-30 00:18:02 +01:00
Peter Steinberger
f3ea2982f5 test(realtime): stabilize websocket timeout test 2026-05-30 01:15:31 +02:00
Peter Steinberger
8f389de88f fix(release): build beta smoke REST curl command 2026-05-30 00:12:11 +01:00
Peter Steinberger
2bcba64906 fix(release): avoid gh api in beta smoke 2026-05-30 00:12:11 +01:00
Peter Steinberger
cbd492d680 fix(feishu): reopen retryable bot menu replay 2026-05-30 00:12:10 +01:00
Peter Steinberger
fadd275e7b fix(release): harden candidate run status polling 2026-05-30 00:11:24 +01:00
Peter Steinberger
35a3c064a7 fix(release): avoid gh api for candidate reads 2026-05-30 00:10:05 +01:00
Peter Steinberger
91adfa1582 fix(telegram): cap polling lease wait timer 2026-05-29 19:07:40 -04:00
Vincent Koc
f3f85ae5f7 refactor: share live transport scenario helpers 2026-05-30 01:05:56 +02:00
Peter Steinberger
69550a9d3d ci: satisfy build profile lint 2026-05-30 00:05:40 +01:00
Peter Steinberger
5b8472b0b9 fix(whatsapp): cap credential flush timeout 2026-05-29 19:03:59 -04:00
Dallin Romney
73dd36626c test(infra): avoid max fake-timer jumps (#88155) 2026-05-29 16:02:41 -07:00
Peter Steinberger
83905c9169 fix(ci): repair main lint gates 2026-05-30 00:01:11 +01:00
Peter Steinberger
d92a0292a9 fix(memory): cap qmd process timeouts 2026-05-29 19:00:05 -04:00
Peter Steinberger
0e6937cc1b ci: skip bundled dts in artifact build 2026-05-29 23:56:31 +01:00
Peter Steinberger
b1e5c9d7fa fix(agents): centralize terminal run outcome precedence (#88136)
* fix(agents): centralize terminal run outcome precedence

* docs(agents): explain terminal outcome precedence

* docs(agents): note terminal outcome helper

* fix(agents): preserve pending hard timeout over late completion

* test(agents): align global session scoping expectation

* Revert "test(agents): align global session scoping expectation"

This reverts commit 9b4a0c3cb1b3885299eea7081d97f7142c415dc2.

* test(infra): stabilize CONNECT timeout cap test

* fix(agents): prioritize hard timeout terminal evidence

* fix(gateway): preserve pending hard timeout snapshots
2026-05-30 00:56:20 +02:00
Vincent Koc
ba3eae5518 fix(dev): cap Discord smoke response bodies 2026-05-30 00:54:23 +02:00
Peter Steinberger
60673b03bc fix(zalouser): cap qr login timeouts 2026-05-29 18:54:18 -04:00
Peter Steinberger
d5e8da8499 fix(ci): repair main normalization checks 2026-05-29 23:53:28 +01:00
keshavbotagent
5f89fbe669 fix(codex): recover app-server completion stalls
Fix Codex app-server completion-stall recovery so replay-safe stdio completion-idle failures retry once, while progress/terminal turn-watch timeouts only surface timeout payloads.

Also preserve post-tool completion guards for scoped native response deltas and stabilize the oversized CONNECT timeout regression test picked up from latest main.

Co-authored-by: Kelaw - Keshav's Agent <keshavbotagent@gmail.com>
2026-05-30 00:52:48 +02:00
Peter Steinberger
bc848b367f refactor: add shared sqlite state database
Adds the shared SQLite state database base, moves plugin keyed state into it with doctor migration coverage, and keeps generated Kysely guardrails aligned. Proof: focused SQLite/plugin-state tests, db:kysely:check, lint:kysely, architecture/dependency guards, autoreview, and PR CI all clean.
2026-05-30 00:52:23 +02:00
Peter Steinberger
a6a99b923e fix(zalouser): cap probe timeout timer 2026-05-29 18:48:43 -04:00
Peter Steinberger
ccad5d7b63 fix(web): cap guarded fetch timeout seconds 2026-05-29 18:45:30 -04:00
Peter Steinberger
42b4715124 test(infra): preserve script wrapper fixture 2026-05-30 00:42:41 +02:00
Peter Steinberger
465c4cb580 test(infra): stabilize main CI tests 2026-05-30 00:42:41 +02:00
Peter Steinberger
37ccec0dc7 fix(nostr): cap profile import relay timers 2026-05-29 18:40:17 -04:00
Peter Steinberger
cb4d2e7bb9 test: stabilize infra state shard 2026-05-29 23:38:31 +01:00
Peter Steinberger
41a92ae445 perf: resolve native esm plugin sdk imports 2026-05-29 23:38:08 +01:00
Peter Steinberger
d7354d61b2 fix(channels): centralize stall watchdog timer bounds 2026-05-29 18:35:37 -04:00
Kevin Lin
c57671176e refactor: share native approval route gates
Share native approval route gate helpers across mainstream channel approval runtimes and keep PR #87770 green on current main.
2026-05-29 15:32:31 -07:00
Peter Steinberger
44e31f7c6a test(gateway): stabilize live helper shard 2026-05-30 00:31:07 +02:00
Peter Steinberger
63a06e312d ci: reduce main workflow critical path 2026-05-29 23:29:32 +01:00
Peter Steinberger
ed9e9aab3d fix(infra): cap transport readiness timeouts 2026-05-29 18:28:15 -04:00
Vincent Koc
dfe99e9cd7 refactor: share media understanding post params 2026-05-30 00:27:13 +02:00
Vincent Koc
9331ac2cb0 fix(scripts): cap issue labeler response bodies 2026-05-30 00:25:51 +02:00
Peter Steinberger
7f28c8bd07 fix: route media completions through requester agent (#88141) 2026-05-30 00:24:28 +02:00
Peter Steinberger
bafa6de76d fix(proxy): cap connect tunnel timeouts 2026-05-29 18:24:03 -04:00
Sally O'Malley
6037a74660 Add plugin manifest contract for SecretRef provider integrations (#82326)
* secret-provider-integrations

Signed-off-by: sallyom <somalley@redhat.com>

* feat(secrets): configure plugin provider presets

* secrets: use plugin-managed provider refs

Signed-off-by: sallyom <somalley@redhat.com>

* fix secretref auth profile service env

* test secret provider integration e2e

* fix secretref plugin config service env

* fix secret provider preset schema alignment

* stabilize secret provider service proof

* validate secret provider plugin integrations

* harden secret provider resolver paths

* scope secret provider config validation

* stabilize openai secret provider proof

* fix secret provider metadata proof

* stabilize config baseline proof

* fix secret provider e2e lint

---------

Signed-off-by: sallyom <somalley@redhat.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
2026-05-29 18:20:45 -04:00
Peter Steinberger
f1235477de fix(apns): cap direct timeout paths 2026-05-29 18:18:33 -04:00
Peter Steinberger
526925c509 test: stabilize remaining CI flakes 2026-05-29 23:17:36 +01:00
Peter Steinberger
3204efc195 fix(infra): cap shell env timeouts 2026-05-29 18:11:50 -04:00
Peter Steinberger
2860da8cd5 fix(infra): cap jsonl socket timeouts 2026-05-29 18:07:19 -04:00
Peter Steinberger
8f2e520abb fix(apns): cap relay timeout 2026-05-29 18:03:41 -04:00
Peter Steinberger
fe3f2bee3f test: fix main CI regressions 2026-05-29 23:03:01 +01:00
Peter Steinberger
a51e8a21b6 fix(ci): break skills loading cycle 2026-05-30 00:02:24 +02:00
Peter Steinberger
260e8e26fd fix(ci): repair main checks 2026-05-30 00:02:24 +02:00
Vincent Koc
196ea61ec4 refactor: share diagnostics timeline span helpers 2026-05-30 00:01:58 +02:00
Vincent Koc
49cc613021 fix(supervisor): narrow stored session limit parsing 2026-05-30 00:01:47 +02:00
Peter Steinberger
347486a4c4 fix(openai): cap codex oauth preflight timeout 2026-05-29 17:59:29 -04:00
Peter Steinberger
1517fe2c32 perf: prefer package-local bundled plugin artifacts 2026-05-29 22:57:40 +01:00
Peter Steinberger
fe69df6b3a fix(gateway-client): cap stop wait timeout 2026-05-29 17:55:17 -04:00
Shakker
dac67b3978 test: complete skills status mock surface 2026-05-29 22:51:15 +01:00
Shakker
a6c694da7e test: remove duplicate skill fixture wrappers 2026-05-29 22:51:15 +01:00
Shakker
259d6aada8 test: share skills entry fixtures 2026-05-29 22:51:15 +01:00
Shakker
de6aaf8e23 test: preserve real skills status exports 2026-05-29 22:51:15 +01:00
Shakker
496e1e071f perf: use set for bundled skill allowlist 2026-05-29 22:51:15 +01:00
Shakker
112939df60 perf: prepare bundled skill allowlist once 2026-05-29 22:51:15 +01:00
Shakker
e8cece82ef perf: speed up skills filtering 2026-05-29 22:51:15 +01:00
Shakker
93c68c4432 perf: reuse resolved skills allowlist 2026-05-29 22:51:15 +01:00
Shakker
2009bec87a refactor: reuse shared skills prompt formatter 2026-05-29 22:51:15 +01:00
Shakker
f382a36458 perf: centralize skill status lookup 2026-05-29 22:51:15 +01:00
Shakker
45b12c0085 refactor: share skill command exposure policy 2026-05-29 22:51:15 +01:00
Shakker
0b86591d9d perf: avoid unnecessary skills index maps 2026-05-29 22:51:15 +01:00
Shakker
1221414709 feat: add skills index 2026-05-29 22:51:15 +01:00
Peter Steinberger
1c8de09ba9 ci: stabilize main checks 2026-05-29 22:49:06 +01:00
Peter Steinberger
7cd93f8e5c fix(infra): cap request body timeouts 2026-05-29 17:48:40 -04:00
Dallin Romney
1dbde826f2 fix ci mainline checks (#88137) 2026-05-29 14:41:30 -07:00
Peter Steinberger
1d84255581 fix(media): cap generation provider timeouts 2026-05-29 17:36:53 -04:00
Peter Steinberger
e1c88d4425 fix(tts): cap speech provider timeouts 2026-05-29 17:31:37 -04:00
Vincent Koc
e69fedc8cf refactor: share media temp save wrapper 2026-05-29 23:24:56 +02:00
Peter Steinberger
a841778b7b fix(acp): cap turn timeout timers 2026-05-29 17:20:48 -04:00
Peter Steinberger
522d0f7ef5 perf: reuse gateway runtime metadata 2026-05-29 22:16:53 +01:00
Peter Steinberger
50378c01e4 fix(discord): cap monitor helper timeouts 2026-05-29 17:15:28 -04:00
Peter Steinberger
3416edf740 fix(codex-supervisor): centralize session limit parsing 2026-05-29 17:10:38 -04:00
Peter Steinberger
040f14b641 fix(browser): cap node runtime timeouts 2026-05-29 17:07:33 -04:00
Peter Steinberger
8c53d100ca fix(ci): repair main checks 2026-05-29 23:05:54 +02:00
Peter Steinberger
5230a23202 fix(browser): cap control fetch timeouts 2026-05-29 17:04:43 -04:00
Peter Steinberger
6443d06764 fix: move compaction planning off the event loop
Move compaction planning work to a bounded worker-thread path so large transcript planning no longer monopolizes the agent event loop. Extract pure planning helpers, sanitize worker inputs before structured clone, package the worker entrypoint, and keep synchronous fallback only for worker-unavailable cases.

Fixes #86358.
2026-05-29 23:04:23 +02:00
Vincent Koc
6fd8cfd5bb refactor: share script bounded response reader 2026-05-29 23:02:03 +02:00
Peter Steinberger
95f9231136 fix(feishu): cap async helper timeouts 2026-05-29 17:01:11 -04:00
Peter Steinberger
e6b011823e fix(signal): cap client request timeouts 2026-05-29 16:57:04 -04:00
Peter Steinberger
31169ff3b4 fix: bound default heartbeat run timeout (#88133)
Fixes #87438.

Bound unset heartbeat run timeouts so background heartbeat turns no longer inherit the built-in 48-hour interactive agent default. Timeout precedence is explicit heartbeat timeout, explicit global agent timeout, then heartbeat cadence capped at 600 seconds.

Verification:
- git diff --check
- Testbox tbx_01kstna69zvznn4fq7zrqr04a1: corepack pnpm test src/infra/heartbeat-runner.model-override.test.ts -- --reporter=verbose passed 13 tests
- Direct node --import tsx runtime probe verified 300s, 600s, 60s, and 45s timeout precedence cases
- Autoreview clean

Known CI state:
- PR CI run 26661465248 has failures matching latest main CI run 26661386468 at a7820b2f54; failures are outside this six-file heartbeat/docs diff.
2026-05-29 22:56:13 +02:00
Peter Steinberger
7f09d6ae48 fix(usage): cap provider usage fetch timeouts 2026-05-29 16:53:07 -04:00
Peter Steinberger
a7820b2f54 fix(provider): cap operation timeouts 2026-05-29 16:47:36 -04:00
Vincent Koc
150673a734 refactor: share script budget number parsing 2026-05-29 22:44:38 +02:00
Peter Steinberger
b7e9272dbe fix(agents): cap model scan timeouts 2026-05-29 16:43:03 -04:00
Peter Steinberger
0b86decf94 fix: keep live OpenClaw session locks during cleanup (#88129)
Keep session lock cleanup from removing live OpenClaw-owned locks solely because they are old. Cleanup now reports age-only stale locks without deleting them, while still removing dead, orphaned, recycled, malformed-old, and non-OpenClaw-owned locks.

Update doctor docs and regression coverage for the cleanup/repair contract.

Refs #87779
2026-05-29 22:42:04 +02:00
Peter Steinberger
61e7b042b6 fix(crestodian): cap probe timeouts 2026-05-29 16:38:45 -04:00
Peter Steinberger
d10fd6b8f4 test: fix timeout mock return types 2026-05-29 16:38:45 -04:00
Peter Steinberger
a509c48f0e feat: add core session goals (#87469)
* feat: add core session goals

* feat: polish session goals in tui

* fix: resolve goal tool session stores

* fix: keep get goal read-only

* fix: migrate legacy goal session slots

* fix: persist goal token accounting

* fix: validate goal session rows

* refactor: remove unshipped goal legacy handling

* fix: handle goal commands in local tui

* fix: satisfy goal tool display checks

* fix: reset goal budget on overdue resume

* feat: surface session goals across control surfaces

* test: update gateway protocol test import

* test: align goal fixture types with protocol

* fix: scope selected global transcript usage fallback

* fix: scope selected global web subscriptions

* fix: preserve selected global agent during chat dispatch

* fix: scope chat inject to selected global agents
2026-05-29 22:36:29 +02:00
Peter Steinberger
057be10e5b perf: reuse provider handles and strict tool schemas 2026-05-29 21:34:59 +01:00
Peter Steinberger
b832975f3e fix(mattermost): cap dm retry timeouts 2026-05-29 16:31:01 -04:00
Peter Steinberger
26ea53cc68 fix(zai): cap endpoint probe timeouts 2026-05-29 16:28:33 -04:00
Peter Steinberger
57aec8c565 docs(skills): require grouped release changelogs 2026-05-29 21:28:06 +01:00
Vincent Koc
be6cac375a refactor: share e2e mock http helpers 2026-05-29 22:26:17 +02:00
Peter Steinberger
6e125adf3a fix(xiaomi): cap tts request timeouts 2026-05-29 16:25:32 -04:00
Peter Steinberger
0983e763fe fix(qa-matrix): cap substrate request timeouts 2026-05-29 16:22:33 -04:00
Peter Steinberger
69c3b56bde fix: stabilize codex supervisor session listing 2026-05-29 21:20:00 +01:00
Peter Steinberger
f66d14def5 fix(zalo): cap api request timeouts 2026-05-29 16:19:18 -04:00
Lucas Giordano
eb7e237151 docs(browser): add Notte cloud browser to direct WebSocket CDP providers
Notte exposes a CDP-compatible WebSocket gateway at
wss://us-prod.notte.cc/sessions/connect?token=<NOTTE_API_KEY> that
auto-creates a session on connect — the same shape OpenClaw's existing
"Direct WebSocket CDP providers" section was generically framed for
(per #31085).

Real behaviour proof (against wss://us-prod.notte.cc/sessions/connect):

  $ openclaw browser --browser-profile notte open https://example.com
  opened: https://example.com/
  tab: t4
  id: 7FE04AC44931A6E1C799DE4ABF0DC807

A screenshot captured against the same session is a 1254x1111 PNG of
the rendered example.com page.

Playwright connectOverCDP flow against the same URL (today):

  connectOverCDP                                      695ms
  context.newCDPSession(page)                         169ms
  session.send('Target.getTargetInfo') → targetId     87ms
  page.goto('https://example.com')                    631ms
  total                                               1.8s

AI-assisted (Claude Opus 4.7). codex review --base origin/main returned
clean. See PR description for the full pre-flight checklist.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:17:32 +02:00
Vincent Koc
beb665212c refactor: share e2e bounded response reader 2026-05-29 22:10:14 +02:00
zhang-guiping
689e8ec893 fix(agents): forward ACP spawn attachments
Forward initial image/file attachments when spawning ACP subagents through the existing sessions_spawn attachment opt-in. Remove the PR-only acpEnabled config split so ACP uses the same attachment gate as other runtimes.

Also fix the PR branch CI fallout: type the browser element CLI request mock and use Vitest env stubs in the Azure speech test to satisfy the changed-path security scan.

Verification:
- GitHub CI passed on f6ca26b160.
- Autoreview clean.
- Crabbox AWS live OpenAI proof passed: cbx_a576d49493fe / run_081dcc6c6a1b.

Thanks @zhangguiping-xydt.
2026-05-29 22:08:19 +02:00
Peter Steinberger
f8ad20b87e fix(signal): cap container timeout timers 2026-05-29 16:08:08 -04:00
Nimrod Gutman
6897711d19 feat(ios): add talk tab realtime playback (#88105)
Merged via squash.

Prepared head SHA: f41112a882
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-05-29 23:06:19 +03:00
Peter Steinberger
8ed5ea499d fix: keep compaction timeout snapshots continuable 2026-05-29 22:06:16 +02:00
xin zhuang
960117259d fix(agents): preserve rotated compaction session identity
Fix `sessions.json` persistence after compaction transcript rotation.

When the agent runtime rotates from the pre-compaction session transcript to the post-compaction transcript, post-run consumers now receive the effective OpenClaw session id and session file. Backend CLI session ids remain backend metadata and no longer overwrite the top-level OpenClaw session identity.

Refs #88040.
Thanks @1052326311.

Verification:
- `node scripts/run-vitest.mjs src/agents/agent-command.compaction-rotation.test.ts src/agents/agent-command.live-model-switch.test.ts src/agents/command/session-store.test.ts`
- Autoreview clean
- GitHub CI green on PR head `c3d3c77ddf675bbba0b9ba6681b030a2f69a898c`
2026-05-29 22:05:05 +02:00
Peter Steinberger
4b9a80d895 fix(discord): cap request timeout signals 2026-05-29 16:03:39 -04:00
Peter Steinberger
3b91d18c37 docs(skills): expand Discrawl archive workflow 2026-05-29 22:02:52 +02:00
Peter Steinberger
4f2dc09431 fix(auth): cap GitHub Copilot OAuth timeouts 2026-05-29 22:02:52 +02:00
Peter Steinberger
b3dc7a4a80 fix(exec): bind node auto-review to prepared plans 2026-05-29 22:01:27 +02:00
Peter Steinberger
e2966faea7 perf: reuse gateway session and plugin metadata paths 2026-05-29 21:01:00 +01:00
Peter Steinberger
b245cb2b6d docs(plugins): add external package readmes 2026-05-29 21:00:29 +01:00
Peter Steinberger
2b15850b47 build(plugins): externalize tokenjuice 2026-05-29 21:00:29 +01:00
Peter Steinberger
f10bad944f fix(oauth): cap tls preflight timeout 2026-05-29 15:59:27 -04:00
Peter Steinberger
fb8b9e9138 fix(copilot): cap oauth request timeouts 2026-05-29 15:54:28 -04:00
Dallin Romney
e848671e9d test(ci): fix main test expectations (#88122) 2026-05-29 12:53:30 -07:00
Vincent Koc
b1719474d5 refactor: share e2e incremental line reader 2026-05-29 21:51:46 +02:00
Peter Steinberger
c8f5a2e0e2 fix(qa-lab): cap credential broker request timeouts 2026-05-29 15:49:38 -04:00
Peter Steinberger
c4e1bb30da fix: close native hook relay replacement race 2026-05-29 21:47:14 +02:00
Peter Steinberger
1e2fda9e68 docs(plugins): clarify external plugin installs 2026-05-29 20:43:51 +01:00
Vincent Koc
7d0347b6de refactor: share ui chat send wrapper 2026-05-29 21:38:29 +02:00
Peter Steinberger
a0c1f5962d fix(runtime): centralize safe timer timeout resolution 2026-05-29 15:36:38 -04:00
Vincent Koc
33b81686ad test(file-transfer): remove stale tar fixture awaits 2026-05-29 21:23:11 +02:00
Vincent Koc
07870dff45 refactor: share codex app server start context 2026-05-29 21:19:55 +02:00
Peter Steinberger
99b24a80fb build(plugins): externalize copilot runtime 2026-05-29 20:14:38 +01:00
Peter Steinberger
a39c2d784e fix(minimax): cap tts timeout delays 2026-05-29 15:11:01 -04:00
Nimrod Gutman
0167f0a6df feat(ios): default to hosted push relay (#88096)
Merged via squash.

Prepared head SHA: 75f939af5c
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Co-authored-by: ngutman <1540134+ngutman@users.noreply.github.com>
Reviewed-by: @ngutman
2026-05-29 22:05:25 +03:00
Peter Steinberger
11e82bdef2 fix(lmstudio): cap model fetch timeout delays 2026-05-29 15:05:20 -04:00
Vincent Koc
7aca070723 fix(scripts): cap gh-read json bodies 2026-05-29 21:01:37 +02:00
Peter Steinberger
e5845dd452 fix(codex): cap responses request timeout delays 2026-05-29 14:59:37 -04:00
Vincent Koc
ba55b3e360 refactor: share script bounded response helper 2026-05-29 20:54:29 +02:00
Peter Steinberger
467b068fdc perf(sessions): patch single-entry store writes 2026-05-29 19:54:01 +01:00
Peter Steinberger
18bfd44439 test: shard channel import guardrails 2026-05-29 20:52:19 +02:00
Peter Steinberger
fb18f95348 test: stabilize slow assertion timings 2026-05-29 20:52:19 +02:00
Peter Steinberger
7f4338d435 test: speed up slow assertions 2026-05-29 20:52:18 +02:00
Peter Steinberger
16cd7f9d3f fix(oauth): cap request abort timeout delays 2026-05-29 14:52:01 -04:00
Peter Steinberger
4e2d9b0b76 fix(providers): cap model request timeout delays 2026-05-29 14:43:32 -04:00
Vincent Koc
040eba1cdc refactor: share bounded response reader 2026-05-29 20:34:12 +02:00
Vincent Koc
18d2bc441c fix(e2e): harden kitchen sink probe body caps 2026-05-29 20:31:54 +02:00
Peter Steinberger
75ef73d4f7 fix(talk): cap fast context timeout delay 2026-05-29 14:30:59 -04:00
Peter Steinberger
f440121a49 fix(node-host): cap timeout wrapper delays 2026-05-29 14:25:28 -04:00
Peter Steinberger
1ca7f5c0a0 perf(gateway): reuse session maintenance config during turns 2026-05-29 19:23:28 +01:00
Peter Steinberger
61031d1b1c feat(workboard): add agent coordination tools
Summary:
- Add Workboard agent coordination tools for list/read/claim/heartbeat/release/comment/proof/unblock flows.
- Store artifacts, claims, diagnostics, and notifications in the Workboard SQLite-backed plugin state; surface the new metadata through Gateway, Control UI, docs, and plugin manifest contracts.
- Add scoped claim authorization, token redaction, stale diagnostic cleanup, atomic proof artifact writes, and generated i18n metadata.

Verification:
- pnpm test ui/src/i18n/test/translate.test.ts extensions/browser/src/cli/browser-cli-actions-input/register.element.test.ts extensions/workboard/src/store.test.ts extensions/workboard/src/gateway.test.ts extensions/workboard/src/tools.test.ts ui/src/ui/controllers/workboard.test.ts ui/src/ui/views/workboard.test.ts
- pnpm ui:i18n:check
- env -u OPENCLAW_TESTBOX pnpm check:changed
- autoreview --mode local: clean
- PR CI passed; Windows checkout failure rerun passed on attempt 2
2026-05-29 20:23:21 +02:00
Peter Steinberger
afa6b81120 fix(sandbox): bound novnc observer token ttl 2026-05-29 14:20:18 -04:00
Peter Steinberger
4eeb7bfa57 fix(retry): cap unsafe retry delays 2026-05-29 14:15:38 -04:00
Vincent Koc
aae13f4dd2 refactor: share qa report arg parsing 2026-05-29 20:07:53 +02:00
Peter Steinberger
4305fb7cdf fix(auth): reject unsafe wham reset windows 2026-05-29 14:05:14 -04:00
Vincent Koc
e8217cbb7a fix(scripts): cap npm packument reads 2026-05-29 20:01:02 +02:00
Peter Steinberger
e3be541a6c fix(google): reject unsafe vertex adc lifetimes 2026-05-29 13:57:34 -04:00
Peter Steinberger
b9d7dd4a84 fix(feishu): normalize app registration poll timers 2026-05-29 13:53:05 -04:00
763 changed files with 43013 additions and 7387 deletions

View File

@@ -1,6 +1,6 @@
---
name: discrawl
description: "Discord archive: search, sync freshness, DMs, channel slices, SQL counts, and Discrawl repo work."
description: "Discord archive: search, sync freshness, DMs, summaries, TUI, repo/release work."
metadata:
openclaw:
homepage: https://github.com/openclaw/discrawl
@@ -16,29 +16,154 @@ metadata:
# Discrawl
Use local Discord archive data before live Discord APIs. Check freshness for recent/current questions:
Use local Discord archive data first for Discord questions. Hit Discord APIs
only when the archive is stale, missing the requested scope, or the user asks
for current external context.
## Sources
- DB: platform-native XDG data dir, usually
`${XDG_DATA_HOME:-~/.local/share}/discrawl/discrawl.db` on Linux or
`~/Library/Application Support/discrawl/discrawl.db` on macOS
- Config: platform-native XDG config dir, with legacy fallback to
`~/.discrawl/config.toml`
- Cache: platform-native XDG cache dir
- Logs: platform-native XDG state dir
- Git share repo: platform-native XDG data dir
- Repo: `openclaw/discrawl`; use `~/GIT/_Perso/discrawl` only after verifying
its remote targets `openclaw/discrawl`, otherwise use a fresh checkout
- Preferred CLI: `discrawl`; fallback to `go run ./cmd/discrawl` from the repo
if the installed binary is stale
## Freshness
For recent/current questions, check freshness before analysis:
```bash
discrawl status --json
```
For precise freshness from the default database:
```bash
# Discrawl uses macOS ~/Library defaults unless XDG_DATA_HOME is explicitly set.
case "$(uname -s)" in
Darwin)
db="$HOME/Library/Application Support/discrawl/discrawl.db"
;;
*)
db="${XDG_DATA_HOME:-$HOME/.local/share}/discrawl/discrawl.db"
;;
esac
sqlite3 "$db" \
"select coalesce(max(updated_at),'') from sync_state where scope like 'channel:%';"
```
Routine diagnostics:
```bash
discrawl doctor
```
Refresh only when stale or asked:
Desktop-local refresh:
```bash
discrawl sync --source wiretap
```
Bot API latest refresh, when credentials are available:
```bash
discrawl sync
```
Query with bounded slices:
Use `--full` only for deliberate historical backfills:
```bash
discrawl sync --full
```
If SQLite reports busy/locked, check for stray `discrawl` processes before retrying.
## Query Workflow
1. Resolve scope: guild, channel, DM, author, keyword, date range.
2. Check freshness for recent/current requests.
3. Prefer CLI search/messages for slices; use read-only SQL for exact counts.
4. Report absolute date spans, counts, channel/DM names, and known gaps.
Use root or subcommand help for syntax: `discrawl --help`,
`discrawl help search`, `discrawl search --help`. Use
`DISCRAWL_NO_AUTO_UPDATE=1` for read smokes when you do not want git-share
updates.
Common commands:
```bash
DISCRAWL_NO_AUTO_UPDATE=1 discrawl search --limit 20 "query"
discrawl messages --channel '#maintainers' --days 7 --all
discrawl dms --last 20
discrawl tui --dm
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select count(*) from messages;"
```
Report absolute date spans, channel/DM names, counts, and known gaps. Use read-only SQL for exact counts/rankings. Never use `--unsafe --confirm` unless the user explicitly requests a reviewed DB mutation.
## SQL
Boundaries: bot sync needs configured Discord bot credentials. Wiretap reads local Discord Desktop artifacts only; do not extract user tokens, call Discord as the user, or write to Discord storage. Git-share snapshots must not include secrets or `@me` DM rows.
Use `discrawl sql` for exact counts, joins, and ranking queries when normal
CLI reads are too coarse. The command is read-only by default, accepts SQL as
args or stdin, and supports `--json` for agent parsing.
Useful examples:
```bash
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select count(*) as messages from messages;"
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select coalesce(nullif(c.name, ''), m.channel_id) as channel, count(*) as messages from messages m left join channels c on c.id = m.channel_id group by m.channel_id order by messages desc limit 20;"
DISCRAWL_NO_AUTO_UPDATE=1 discrawl --json sql "select coalesce(nullif(mm.display_name, ''), nullif(mm.global_name, ''), nullif(mm.username, ''), m.author_id) as author, count(*) as messages from messages m left join members mm on mm.guild_id = m.guild_id and mm.user_id = m.author_id group by m.guild_id, m.author_id order by messages desc limit 20;"
```
Never use `--unsafe --confirm` unless the user explicitly asks for a database
mutation and the write has been reviewed.
When the installed CLI lacks a new feature, build or run from a verified
`openclaw/discrawl` checkout before concluding the feature is missing.
## Discord Boundaries
Bot API sync requires configured Discord bot credentials; do not invent token
availability. Desktop wiretap mode reads local Discord Desktop artifacts and
must not extract credentials, use user tokens, call Discord as the user, or
write to Discord application storage. Wiretap/Desktop cache DMs are local-only
and must not be described as part of the published Git snapshot. Git-share
snapshots must not include secrets or `@me` DM rows.
## Verification
For repo edits, prefer existing Go gates:
```bash
GOWORK=off go test ./...
```
Then run targeted CLI smoke for the touched surface, for example:
```bash
discrawl doctor
discrawl status --json
DISCRAWL_NO_AUTO_UPDATE=1 discrawl search --limit 5 "test"
```
## ClawSweeper Sandbox
Use the sandbox reader only:
```bash
discrawl-sandbox search --limit 20 "query"
discrawl-sandbox messages --channel clawtributors --days 7 --all
discrawl-sandbox status --json
```
This reader imports `https://github.com/openclaw/discord-store.git` into
`/root/clawsweeper-sandbox-workspace/.discrawl/discrawl.db` with
`discord.token_source = "none"`. The published Git snapshot is public-channel
filtered; do not use `/root/.discrawl/config.toml` or the rich writer DB from
sandboxed public Discord sessions.

View File

@@ -6,14 +6,16 @@ description: Regenerate OpenClaw release changelog sections from git history bef
# OpenClaw Changelog Update
Use this for release changelog rewrites and GitHub release-note source text.
Use it with `release-openclaw-maintainer`; this skill owns changelog content,
ordering, and audit discipline.
This is mandatory before every beta, beta rerun, stable release, or stable
rerun. Use it with `release-openclaw-maintainer`; this skill owns changelog
content, ordering, grouping, and attribution discipline.
## Goal
Rewrite the target `CHANGELOG.md` version section from history, not from stale
draft notes. Produce user-facing release notes sorted by user interest while
preserving issue/PR refs and thanks.
draft notes. Produce grouped user-facing release notes sorted by user interest
while preserving every relevant issue/PR ref and every human `Thanks @...`
attribution.
## Inputs
@@ -44,10 +46,18 @@ preserving issue/PR refs and thanks.
- `### Highlights`: 5-8 bullets, broad user wins first
- `### Changes`: new capabilities and behavior changes
- `### Fixes`: user-facing fixes first, grouped by impact and surface
- group related changes/fixes by surface and user impact; avoid one bullet
per tiny commit when several commits tell one user-facing story
6. Preserve attribution:
- keep `#issue`, `(#PR)`, `Fixes #...`, and `Thanks @...`
- every human-authored merged PR represented by a user-facing entry needs
its PR ref and `Thanks @author`, even when the PR had no linked issue
- when grouping multiple PRs/issues in one bullet, include every relevant
PR/issue ref and every human contributor handle in that same bullet
- multiple `Thanks @...` handles in one bullet are expected; do not drop or
collapse contributor credit just because the note is grouped
- if one grouped bullet covers both direct commits and PRs, keep all PR refs
and thanks, plus any issue refs from the direct commits
- do not add GHSA references, advisory IDs, or security advisory slugs to
changelog entries or GitHub release-note text unless explicitly requested
- never thank bots, `@openclaw`, `@clawsweeper`, or `@steipete`

View File

@@ -21,6 +21,30 @@ function jsonGh(args) {
return JSON.parse(gh(args));
}
function githubRestJson(pathSuffix) {
const result = execFileSync(
"bash",
[
"-lc",
[
"set -euo pipefail",
'token="$(gh auth token)"',
'curl -fsS -H "Authorization: Bearer ${token}" -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" "${OPENCLAW_GITHUB_REST_URL}"',
].join("\n"),
],
{
encoding: "utf8",
env: {
...process.env,
OPENCLAW_GITHUB_REST_URL: `https://api.github.com/repos/${repo}/${pathSuffix}`,
},
maxBuffer: 16 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
},
);
return JSON.parse(result);
}
function rate() {
try {
return jsonGh(["api", "rate_limit"]).resources.core;
@@ -59,12 +83,30 @@ for (const job of parent.jobs ?? []) {
}
const since = parent.createdAt;
const runList = gh([
"api",
`repos/${repo}/actions/runs?per_page=100`,
"--jq",
`.workflow_runs[] | select(.created_at >= "${since}") | select(.name=="CI" or .name=="OpenClaw Release Checks" or .name=="Plugin Prerelease" or .name=="NPM Telegram Beta E2E" or .name=="Full Release Validation") | [.id,.name,.status,.conclusion,.head_sha,.html_url] | @tsv`,
]).trim();
const runsQuery = new URLSearchParams({
per_page: "100",
created: `>=${since}`,
exclude_pull_requests: "true",
});
const childWorkflowNames = new Set([
"CI",
"OpenClaw Release Checks",
"Plugin Prerelease",
"NPM Telegram Beta E2E",
"Full Release Validation",
]);
const runs = githubRestJson(`actions/runs?${runsQuery.toString()}`).workflow_runs ?? [];
const runList = runs
.filter(
(run) =>
run.created_at >= since &&
run.head_sha === parent.headSha &&
childWorkflowNames.has(run.name),
)
.map((run) =>
[run.id, run.name, run.status, run.conclusion ?? "", run.head_sha, run.html_url].join("\t"),
)
.join("\n");
if (!runList) {
console.log("children: none found yet");

View File

@@ -69,9 +69,13 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
or clawgrit reports. Report regressions explicitly. A major regression is a
release blocker unless the operator waives it or the data clearly proves
infrastructure noise.
- Generate the changelog before version/tag preparation so the top changelog
section is deduped and ordered by user impact. Use
`$openclaw-changelog-update` for the rewrite.
- Generate the changelog before every beta, beta rerun, stable release, or
stable rerun, before version/tag preparation. Use
`$openclaw-changelog-update` for the rewrite. Do not continue release prep if
the target `CHANGELOG.md` section does not have `### Highlights`,
`### Changes`, and `### Fixes`, grouped by user-facing surface while
preserving every relevant PR/issue ref and every human `Thanks @...`
attribution in the grouped bullet.
- Do not create beta-specific `CHANGELOG.md` headings. Beta releases use the
stable base version section, for example `v2026.4.20-beta.1` uses
`## 2026.4.20` release notes.
@@ -144,6 +148,9 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
section from history, not existing notes. Use the last reachable stable or
beta release tag as the base, then inspect every commit through the target
release SHA.
- The changelog rewrite is not optional for beta reruns: any `beta.N` after a
rebase or backport must refresh the same stable-base `## YYYY.M.D` section
before the new version/tag commit.
- Include both merged PR commits and direct commits on `main`. Direct commits
matter: infer notes from their subject, body, touched files, linked issues,
tests, and nearby code when no PR body exists.
@@ -157,6 +164,11 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
- Add missed user-facing changes, remove internal-only noise, dedupe overlapping
PR/direct-commit entries, and sort each section from most to least interesting
for users.
- Group related highlights, changes, and fixes by user-facing surface and
impact, but never lose traceability: each grouped bullet keeps every relevant
`#issue`, `(#PR)`, `Fixes #...`, and every human `Thanks @...` handle.
Multiple thanks in one bullet are expected when multiple contributor PRs are
grouped.
- Changelog entries should be user-facing, not internal release-process notes.
- GitHub release and prerelease bodies must use the full matching
`CHANGELOG.md` version section, not highlights or an excerpt. When creating

View File

@@ -28,7 +28,7 @@ permissions:
concurrency:
group: ${{ github.event_name == 'workflow_dispatch' && format('{0}-manual-v1-{1}', github.workflow, github.run_id) || (github.event_name == 'pull_request' && format('{0}-v7-{1}', github.workflow, github.event.pull_request.number) || (github.repository == 'openclaw/openclaw' && format('{0}-v7-{1}', github.workflow, github.ref) || format('{0}-v7-{1}-{2}', github.workflow, github.ref, github.sha))) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || (github.event_name == 'push' && github.repository == 'openclaw/openclaw' && github.ref == 'refs/heads/main') }}
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
@@ -466,8 +466,8 @@ jobs:
- name: Audit production dependencies
run: node scripts/pre-commit/pnpm-audit-prod.mjs --audit-level=high
# Warm the lockfile- and pnpm-pinned store once before Linux Node shards fan out.
# On a cold key this job owns the save, so later shards restore the exact key.
# Warm the lockfile- and pnpm-pinned store without blocking Linux Node shards.
# On a cold key this job owns the save for later workflow runs.
pnpm-store-warmup:
permissions:
contents: read
@@ -532,9 +532,9 @@ jobs:
build-artifacts:
permissions:
contents: read
needs: [preflight, pnpm-store-warmup]
needs: [preflight]
if: needs.preflight.outputs.run_build_artifacts == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-16vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-32vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
outputs:
channels-result: ${{ steps.built_artifact_checks.outputs['channels-result'] }}
@@ -597,6 +597,14 @@ jobs:
with:
install-bun: "false"
- name: Restore build-all step cache
uses: actions/cache@v5
with:
path: .artifacts/build-all-cache
key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
restore-keys: |
${{ runner.os }}-build-all-v3-
- name: Build dist
env:
NODE_OPTIONS: --max-old-space-size=8192
@@ -694,20 +702,6 @@ jobs:
pids+=("$!")
}
if [ "$RUN_GATEWAY_WATCH" = "true" ]; then
gateway_watch_log="${RUNNER_TEMP}/gateway-watch.log"
echo "starting gateway-watch: node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000"
if node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000 >"$gateway_watch_log" 2>&1; then
result="success"
else
result="failure"
fi
echo "::group::gateway-watch log"
cat "$gateway_watch_log"
echo "::endgroup::"
results["gateway-watch"]="$result"
fi
if [ "$RUN_CHANNELS" = "true" ]; then
start_check "channels" env \
NODE_OPTIONS=--max-old-space-size=8192 \
@@ -722,6 +716,11 @@ jobs:
node scripts/run-vitest.mjs run --config test/vitest/vitest.full-core-support-boundary.config.ts
fi
if [ "$RUN_GATEWAY_WATCH" = "true" ]; then
start_check "gateway-watch" \
node scripts/check-gateway-watch-regression.mjs --skip-build --ready-timeout-ms 5000
fi
for index in "${!pids[@]}"; do
name="${names[$index]}"
log="${logs[$index]}"
@@ -764,7 +763,7 @@ jobs:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, pnpm-store-warmup]
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast_core == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 60
@@ -853,7 +852,7 @@ jobs:
permissions:
contents: read
name: ${{ matrix.checkName }}
needs: [preflight, pnpm-store-warmup]
needs: [preflight]
if: needs.preflight.outputs.run_plugin_contracts_shards == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 60
@@ -933,7 +932,7 @@ jobs:
permissions:
contents: read
name: ${{ matrix.checkName }}
needs: [preflight, pnpm-store-warmup]
needs: [preflight]
if: needs.preflight.outputs.run_checks_fast == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 60
@@ -1085,7 +1084,7 @@ jobs:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, pnpm-store-warmup]
needs: [preflight]
if: needs.preflight.outputs.run_checks_node_core_nondist == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-8vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
timeout-minutes: 60
@@ -1191,8 +1190,8 @@ jobs:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, pnpm-store-warmup]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' && needs.pnpm-store-warmup.result == 'success' }}
needs: [preflight]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check == 'true' }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && (matrix.runner || 'blacksmith-4vcpu-ubuntu-2404') || 'ubuntu-24.04') }}
timeout-minutes: 20
strategy:
@@ -1322,8 +1321,8 @@ jobs:
permissions:
contents: read
name: ${{ matrix.check_name }}
needs: [preflight, pnpm-store-warmup]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' && needs.pnpm-store-warmup.result == 'success' }}
needs: [preflight]
if: ${{ !cancelled() && always() && needs.preflight.outputs.run_check_additional == 'true' }}
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
strategy:
@@ -1489,7 +1488,7 @@ jobs:
check-docs:
permissions:
contents: read
needs: [preflight, pnpm-store-warmup]
needs: [preflight]
if: needs.preflight.outputs.run_check_docs == 'true'
runs-on: ${{ github.event_name == 'workflow_dispatch' && 'ubuntu-24.04' || (github.repository == 'openclaw/openclaw' && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-24.04') }}
timeout-minutes: 20
@@ -2114,7 +2113,7 @@ jobs:
- macos-node
- macos-swift
- android
if: ${{ !cancelled() && always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
if: ${{ !cancelled() && always() && github.event_name != 'push' && (github.event_name != 'pull_request' || !github.event.pull_request.draft) }}
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:

View File

@@ -72,6 +72,7 @@ Skills own workflows; root owns hard policy and routing.
- Plugin SDK exception: shipped external API gets new API first plus named compat/deprecation, small tests/docs if useful, removal plan.
- Migrate internal/bundled callers to modern API in the same change. Do not let internal compat become permanent architecture.
- Channels are implementation under `src/channels/**`; plugin authors get SDK seams. Providers own auth/catalog/runtime hooks; core owns generic loop.
- Agent run terminal state: normalize/merge via `src/agents/agent-run-terminal-outcome.ts`; do not rederive timeout/cancel precedence in projections.
- Hot paths should carry prepared facts forward: provider id, model ref, channel id, target, capability family, attachment class. Do not rediscover with broad plugin/provider/channel/capability loaders.
- Do not fix repeated request-time discovery with scattered caches. Move the canonical fact earlier; reuse prepared runtime objects; delete duplicate lookup branches.
- Gateway/plugin metadata is process-stable: installs, manifests, catalogs, generated paths, bundled metadata. Changes require restart or explicit owner reload/install/doctor flow.
@@ -227,6 +228,7 @@ Skills own workflows; root owns hard policy and routing.
- Parallels: `$openclaw-parallels-smoke`; Discord roundtrip: `$parallels-discord-roundtrip`.
- Crabbox/WebVNC human demos: keep remote desktop visible/windowed; no fullscreen remote browser unless video/capture-style output.
- ClawSweeper ops: `$clawsweeper`. Deployed hook sessions may post one concise `#clawsweeper` note only when surprising/actionable/risky; if using message tool, reply exactly `NO_REPLY`.
- Generated-media completions wake the requester agent first. Requester visible-reply config decides final text vs message tool; direct media send is fallback/recovery only.
- Memory wiki prompt digest stays tiny; prefer `wiki_search` / `wiki_get`; verify contact data before use; source-class provenance for generated people facts.
- Rebrand/migration/config warnings: run `openclaw doctor`.
- Never edit `node_modules`.

View File

@@ -4,9 +4,16 @@ Docs: https://docs.openclaw.ai
## Unreleased
### Changes
- Plugins: externalize Tokenjuice as the official `@openclaw/tokenjuice` plugin with npm and ClawHub publish metadata.
- Plugins: externalize the GitHub Copilot agent runtime as the official `@openclaw/copilot` plugin with npm and ClawHub publish metadata.
### Fixes
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
- Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.
## 2026.5.28
@@ -35,6 +42,7 @@ Docs: https://docs.openclaw.ai
- Tighten phone-control mutation authorization [AI]. (#87150) Thanks @pgondhi987.
- Clarify directive persistence authorization policy [AI]. (#86369) Thanks @pgondhi987.
- Agents/Codex: keep spawned agent cwd/workspace state separated, keep hook context prompt-local, release session locks on timeout abort, avoid session event queue self-wait, preserve shared app-server state across startup or helper failures, keep native hook relay alive across restarts, route workspace memory through tools, resolve Codex runtime models first, report quarantined dynamic tools, format `skills` command output, and bound compaction/steering retries. (#87218, #86875, #86123, #87399, #87375, #87383, #87400) Thanks @mbelinky, @Alix-007, @luoyanglang, @yetval, and @sjf.
- Codex Supervisor: keep real-home app-server MCP session listing on the loaded/state-DB path, bound stored history scans, and close WebSocket probes cleanly.
- Channels: thread canonical session keys into outbound hooks, preserve Matrix room-id case, keep fallback tool warnings mention-inert, retain delivered Slack final replies during late cleanup, continue iMessage polling after denied reactions, suppress duplicate native exec approvals, preserve Telegram SecretRef prompt config, suppress Discord recovered tool warnings, and block untrusted Teams service URLs. (#73706, #75670, #87366, #87451, #87334) Thanks @zeroaltitude, @lukeboyett, @xiaotian, and @eleqtrizit.
- CLI/auth/doctor/providers: reject malformed numeric/timeout/subcommand-version inputs, wait for respawn child shutdown, bound Codex and GitHub Copilot OAuth/token requests, warm provider auth off the main thread, honor Codex response timeouts, bound local service startup, resolve GPT-5.5 without cached catalog, migrate legacy memory auto-provider config, rewrite non-canonical `api_key` auth profiles, and make doctor restart follow-ups actionable. (#87398, #86281, #87361) Thanks @Patrick-Erichsen, @samzong, @giodl73-repo, and @alkor2000.
- Gateway/security/session state: expire browser tokens after auth rotation, scope assistant idempotency dedupe, drain probe client closes, avoid stale restart continuation reuse, preserve retry-after fallbacks, bound webchat image and artifact transcript scans, include seconds in inbound metadata timestamps, and evict current plugin-state namespaces at row caps.

View File

@@ -73,9 +73,10 @@ Release behavior:
- Changing the root gateway version does not change the iOS app version until you explicitly pin from the gateway.
- See `apps/ios/VERSIONING.md` for the full workflow.
Required env for beta builds:
Relay behavior for beta builds:
- `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
- Beta builds default to `https://ios-push-relay.openclaw.ai`.
- Optional custom relay override: `OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com`
This must be a plain `https://host[:port][/path]` base URL without whitespace, query params, fragments, or xcconfig metacharacters.
Archive without upload:
@@ -118,7 +119,7 @@ scripts/ios-asc-keychain-setup.sh \
This should create `apps/ios/fastlane/.env` with the non-secret ASC variables while the private key stays in Keychain.
3. Set the official/TestFlight relay URL for the build:
3. Optional: set a custom official/TestFlight relay URL for the build. If unset, the beta flow uses `https://ios-push-relay.openclaw.ai`.
```bash
export OPENCLAW_PUSH_RELAY_BASE_URL=https://relay.example.com

View File

@@ -0,0 +1,652 @@
import SwiftUI
struct TalkProTab: View {
@Environment(NodeAppModel.self) private var appModel
@AppStorage("talk.enabled") private var talkEnabled: Bool = false
@AppStorage(TalkSpeechLocale.storageKey) private var talkSpeechLocale: String = TalkSpeechLocale.automaticID
@AppStorage(TalkDefaults.speakerphoneEnabledKey) private var talkSpeakerphoneEnabled: Bool =
TalkDefaults.speakerphoneEnabledByDefault
@AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false
@State private var showPermissionPrompt = false
var openSettings: () -> Void
private var state: TalkProState {
TalkProState(
gatewayConnected: self.gatewayConnected,
isEnabled: self.appModel.talkMode.isEnabled || self.talkEnabled,
statusText: self.appModel.talkMode.statusText,
isListening: self.appModel.talkMode.isListening,
isSpeaking: self.appModel.talkMode.isSpeaking,
isUserSpeechDetected: self.appModel.talkMode.isUserSpeechDetected,
permissionState: self.appModel.talkMode.gatewayTalkPermissionState)
}
var body: some View {
NavigationStack {
ZStack {
CommandControlBackground()
ScrollView {
VStack(alignment: .leading, spacing: 10) {
self.header
self.voiceHeroCard
self.conversationCard
self.voiceModeCard
self.controlsCard
}
.padding(.top, 16)
.padding(.bottom, 18)
}
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationBarHidden(true)
}
.sheet(isPresented: self.$showPermissionPrompt) {
NavigationStack {
TalkPermissionPromptView(
style: .sheet,
onPermissionReady: {
self.showPermissionPrompt = false
self.startTalk()
})
.padding()
.navigationTitle("Enable Talk")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Not Now") {
self.showPermissionPrompt = false
}
}
}
}
.presentationDetents([.medium, .large])
.openClawSheetChrome()
}
.onAppear { self.alignPersistedTalkState() }
}
private var header: some View {
HStack(alignment: .center, spacing: 11) {
OpenClawProMark(size: 31, shadowRadius: 9)
VStack(alignment: .leading, spacing: 2) {
Text("Talk")
.font(.system(size: 27, weight: .bold, design: .rounded))
Text(self.headerSubtitle)
.font(.caption.weight(.medium))
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer(minLength: 8)
self.statusChip
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var statusChip: some View {
HStack(spacing: 5) {
Circle()
.fill(self.state.color)
.frame(width: 7, height: 7)
Text(self.state.chipText)
.font(.caption.weight(.bold))
.foregroundStyle(self.state.color)
}
.padding(.horizontal, 10)
.padding(.vertical, 7)
.background {
Capsule(style: .continuous)
.fill(self.state.color.opacity(0.11))
.overlay {
Capsule(style: .continuous)
.strokeBorder(self.state.color.opacity(0.22), lineWidth: 1)
}
}
}
private var voiceHeroCard: some View {
CommandPanel(tint: self.state.color, isProminent: true, padding: 16) {
VStack(alignment: .center, spacing: 16) {
TalkProOrb(
mode: self.state.waveformMode(micLevel: self.appModel.talkMode.micLevel),
color: self.state.color,
systemImage: self.state.icon)
.frame(height: 188)
.accessibilityHidden(true)
VStack(spacing: 5) {
Text(self.state.title)
.font(.title3.weight(.bold))
.multilineTextAlignment(.center)
Text(self.heroSubtitle)
.font(.subheadline.weight(.medium))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
Button(action: self.handlePrimaryAction) {
Label(self.state.primaryButtonTitle, systemImage: self.state.primaryButtonIcon)
.font(.subheadline.weight(.bold))
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.frame(height: 50)
.background {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(self.state.primaryButtonFill)
.shadow(color: self.state.color.opacity(0.28), radius: 18, y: 8)
}
}
.buttonStyle(.plain)
.disabled(self.state.primaryAction == .waiting)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var conversationCard: some View {
CommandPanel(padding: 0) {
VStack(spacing: 0) {
self.cardHeader(title: "Conversation", value: self.state.chipText, color: self.state.color)
.padding(.horizontal, 12)
.padding(.top, 11)
.padding(.bottom, 3)
self.infoRow(icon: "person.crop.circle.fill", title: "Agent", value: self.appModel.activeAgentName)
Divider().padding(.leading, 54)
self.infoRow(
icon: "bubble.left.and.text.bubble.right.fill",
title: "Session",
value: self.appModel.chatSessionKey)
Divider().padding(.leading, 54)
self.infoRow(icon: self.state.icon, title: "Runtime", value: self.appModel.talkMode.statusText)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var voiceModeCard: some View {
CommandPanel(padding: 0) {
VStack(spacing: 0) {
self.cardHeader(
title: "Voice mode",
value: "Settings ",
color: OpenClawBrand.accent,
action: self.openSettings)
.padding(.horizontal, 12)
.padding(.top, 11)
.padding(.bottom, 3)
self.infoRow(icon: "waveform", title: "Mode", value: self.appModel.talkMode.gatewayTalkVoiceModeTitle)
Divider().padding(.leading, 54)
self.infoRow(icon: "antenna.radiowaves.left.and.right", title: "Transport", value: self.transportText)
Divider().padding(.leading, 54)
self.infoRow(icon: "key.fill", title: "Permission", value: self.permissionText)
Divider().padding(.leading, 54)
self.infoRow(icon: "globe", title: "Speech language", value: self.speechLocaleText)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private var controlsCard: some View {
CommandPanel(padding: 0) {
VStack(spacing: 0) {
self.cardHeader(title: "Controls", value: nil, color: .secondary)
.padding(.horizontal, 12)
.padding(.top, 11)
.padding(.bottom, 3)
Toggle("Speakerphone", isOn: self.$talkSpeakerphoneEnabled)
.padding(.horizontal, 14)
.padding(.vertical, 10)
Divider().padding(.leading, 14)
Toggle("Background listening", isOn: self.$talkBackgroundEnabled)
.padding(.horizontal, 14)
.padding(.vertical, 10)
Divider().padding(.leading, 14)
Button(action: self.openSettings) {
HStack {
Label("Voice & Talk settings", systemImage: "slider.horizontal.3")
Spacer()
Image(systemName: "chevron.right")
.font(.caption.weight(.bold))
.foregroundStyle(.secondary)
}
.font(.subheadline.weight(.semibold))
.padding(.horizontal, 14)
.padding(.vertical, 12)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
}
private func cardHeader(
title: String,
value: String?,
color: Color,
action: (() -> Void)? = nil) -> some View
{
HStack(spacing: 8) {
Text(title)
.font(.subheadline.weight(.bold))
Spacer(minLength: 8)
if let value {
if let action {
Button(value, action: action)
.font(.caption.weight(.semibold))
.foregroundStyle(color)
} else {
Text(value)
.font(.caption.weight(.semibold))
.foregroundStyle(color)
}
}
}
}
private func infoRow(icon: String, title: String, value: String) -> some View {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.caption.weight(.bold))
.foregroundStyle(self.state.color)
.frame(width: 30, height: 30)
.background {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(self.state.color.opacity(0.11))
}
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.caption2.weight(.medium))
.foregroundStyle(.secondary)
Text(value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "" : value)
.font(.subheadline.weight(.semibold))
.lineLimit(1)
.minimumScaleFactor(0.78)
}
Spacer(minLength: 0)
}
.padding(.horizontal, 12)
.padding(.vertical, 9)
}
private var gatewayConnected: Bool {
GatewayStatusBuilder.build(appModel: self.appModel) == .connected
}
private var headerSubtitle: String {
let mode = self.appModel.talkMode.gatewayTalkVoiceModeTitle.trimmingCharacters(in: .whitespacesAndNewlines)
let agent = self.appModel.activeAgentName.trimmingCharacters(in: .whitespacesAndNewlines)
if mode.isEmpty || mode == "Not loaded" { return agent.isEmpty ? "Realtime voice" : agent }
if agent.isEmpty { return mode }
return "\(agent)\(mode)"
}
private var heroSubtitle: String {
if self.state
.prefersPermissionCopy { return "Gateway approval is required before this phone can capture voice." }
if !self.gatewayConnected { return "Connect to your gateway to start a voice conversation." }
let subtitle = (self.appModel.talkMode.gatewayTalkVoiceModeSubtitle ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
if !subtitle.isEmpty { return subtitle }
return "Routes voice to \(self.appModel.activeAgentName)."
}
private var transportText: String {
let provider = self.appModel.talkMode.gatewayTalkProviderLabel.trimmingCharacters(in: .whitespacesAndNewlines)
let transport = self.appModel.talkMode.gatewayTalkTransportLabel.trimmingCharacters(in: .whitespacesAndNewlines)
if provider.isEmpty || provider == "Not loaded" { return transport.isEmpty ? "Not loaded" : transport }
if transport.isEmpty || transport == "Not loaded" { return provider }
return "\(provider)\(transport)"
}
private var permissionText: String {
if let failure = self.appModel.talkMode.gatewayTalkPermissionState.failureMessage {
return failure
}
return self.appModel.talkMode.gatewayTalkPermissionState.statusLabel
}
private var speechLocaleText: String {
if self.talkSpeechLocale == TalkSpeechLocale.automaticID { return "Automatic" }
return self.talkSpeechLocale
}
private func alignPersistedTalkState() {
if self.appModel.talkMode.gatewayTalkPermissionState.requiresTalkPermissionAction,
self.talkEnabled || self.appModel.talkMode.isEnabled
{
self.stopTalk()
} else if self.talkEnabled != self.appModel.talkMode.isEnabled {
self.appModel.setTalkEnabled(self.talkEnabled)
}
}
private func handlePrimaryAction() {
switch self.state.primaryAction {
case .start:
self.startTalk()
case .stop:
self.stopTalk()
case .enablePermission:
self.stopTalk()
self.showPermissionPrompt = true
case .openSettings:
self.openSettings()
case .waiting:
break
}
}
private func startTalk() {
self.talkEnabled = true
self.appModel.setTalkEnabled(true)
}
private func stopTalk() {
self.talkEnabled = false
self.appModel.setTalkEnabled(false)
}
}
enum TalkProPrimaryAction: Equatable {
case start
case stop
case enablePermission
case openSettings
case waiting
}
enum TalkProWaveformMode: Equatable {
case level(Double)
case inputSpeech
case speaking
case indeterminate
case still
}
struct TalkProState: Equatable {
let gatewayConnected: Bool
let isEnabled: Bool
let statusText: String
let isListening: Bool
let isSpeaking: Bool
let isUserSpeechDetected: Bool
let permissionState: TalkGatewayPermissionState
private var normalizedStatus: String {
self.statusText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
}
var title: String {
if !self.gatewayConnected { return "Gateway offline" }
switch self.permissionState {
case .missingScope, .requestFailed:
return "Gateway permission required"
case .requestingUpgrade:
return "Requesting approval"
case .upgradeRequested:
return "Approval requested"
case .apiKeyMissing:
return "Voice API key missing"
case .loadFailed:
return "Voice config failed"
default:
break
}
if self.isSpeaking { return "Speaking" }
if self.isListening { return "Listening" }
if self.normalizedStatus.contains("connecting") { return "Connecting" }
if self.normalizedStatus.contains("thinking") { return "Asking OpenClaw" }
if self.isEnabled { return "Ready to talk" }
return "Talk is off"
}
var chipText: String {
if !self.gatewayConnected { return "Offline" }
switch self.permissionState {
case .missingScope, .requestFailed:
return "Needs approval"
case .requestingUpgrade, .upgradeRequested:
return "Pending"
case .apiKeyMissing:
return "API key"
case .loadFailed:
return "Config"
default:
break
}
if self.isSpeaking { return "Speaking" }
if self.isListening { return "Listening" }
if self.isEnabled { return "Ready" }
return "Off"
}
var icon: String {
if !self.gatewayConnected { return "wifi.slash" }
switch self.permissionState {
case .missingScope, .requestFailed:
return "key.fill"
case .requestingUpgrade:
return "paperplane.fill"
case .upgradeRequested:
return "hourglass"
case .apiKeyMissing, .loadFailed:
return "exclamationmark.triangle.fill"
default:
break
}
if self.isSpeaking { return "speaker.wave.2.fill" }
if self.isListening { return "mic.fill" }
if self.normalizedStatus.contains("thinking") { return "sparkles" }
if self.normalizedStatus.contains("connecting") { return "dot.radiowaves.left.and.right" }
return "waveform"
}
var color: Color {
if !self.gatewayConnected { return .secondary }
switch self.permissionState {
case .requestFailed, .loadFailed:
return OpenClawBrand.danger
case .missingScope, .requestingUpgrade, .upgradeRequested, .apiKeyMissing:
return OpenClawBrand.warn
default:
return self.isEnabled ? OpenClawBrand.ok : OpenClawBrand.accentHot
}
}
var primaryAction: TalkProPrimaryAction {
if !self.gatewayConnected { return .openSettings }
switch self.permissionState {
case .missingScope, .requestFailed:
return .enablePermission
case .requestingUpgrade, .upgradeRequested:
return .waiting
case .apiKeyMissing, .loadFailed:
return .openSettings
default:
return self.isEnabled ? .stop : .start
}
}
var primaryButtonTitle: String {
switch self.primaryAction {
case .start: "Start Talk"
case .stop: "Stop Talk"
case .enablePermission: "Enable Talk"
case .openSettings: self.gatewayConnected ? "Open Voice Settings" : "Open Gateway Settings"
case .waiting: "Waiting for Approval"
}
}
var primaryButtonIcon: String {
switch self.primaryAction {
case .start: "play.fill"
case .stop: "stop.fill"
case .enablePermission: "key.fill"
case .openSettings: "gearshape.fill"
case .waiting: "hourglass"
}
}
var primaryButtonFill: AnyShapeStyle {
switch self.primaryAction {
case .stop:
AnyShapeStyle(OpenClawBrand.danger)
case .waiting:
AnyShapeStyle(OpenClawBrand.warn.opacity(0.72))
default:
AnyShapeStyle(LinearGradient(
colors: [self.color.opacity(0.95), OpenClawBrand.accent],
startPoint: .topLeading,
endPoint: .bottomTrailing))
}
}
var prefersPermissionCopy: Bool {
switch self.permissionState {
case .missingScope, .requestingUpgrade, .upgradeRequested, .requestFailed:
true
default:
false
}
}
func waveformMode(micLevel: Double) -> TalkProWaveformMode {
if !self.gatewayConnected { return .still }
switch self.permissionState {
case .requestingUpgrade, .upgradeRequested:
return .indeterminate
case .missingScope, .requestFailed, .apiKeyMissing, .loadFailed:
return .still
default:
break
}
if self.isSpeaking { return .speaking }
if self.isListening, self.isUserSpeechDetected { return .inputSpeech }
if self.isListening { return .level(micLevel) }
if self.normalizedStatus.contains("connecting") || self.normalizedStatus.contains("thinking") {
return .indeterminate
}
return self.isEnabled ? .indeterminate : .still
}
}
private struct TalkProOrb: View {
let mode: TalkProWaveformMode
let color: Color
let systemImage: String
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
TimelineView(.periodic(from: .now, by: 1.0 / 24.0)) { timeline in
ZStack {
ForEach(0..<3, id: \.self) { ring in
Circle()
.strokeBorder(self.color.opacity(self.ringOpacity(ring)), lineWidth: 1.4)
.scaleEffect(self.ringScale(ring, date: timeline.date))
}
Circle()
.fill(self.color.opacity(0.13))
.frame(width: 128, height: 128)
.overlay {
Circle()
.strokeBorder(self.color.opacity(0.30), lineWidth: 1)
}
TalkProWaveform(mode: self.mode, tint: self.color, barCount: 18)
.frame(width: 116, height: 52)
.opacity(self.systemImage == "waveform" || self.systemImage == "mic.fill" ? 1 : 0.34)
Image(systemName: self.systemImage)
.font(.system(size: 34, weight: .bold))
.foregroundStyle(self.color)
.opacity(self.systemImage == "waveform" || self.systemImage == "mic.fill" ? 0.20 : 1)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func ringScale(_ ring: Int, date: Date) -> CGFloat {
guard !self.reduceMotion else { return CGFloat(1.0 + (Double(ring) * 0.12)) }
let base = 0.88 + (Double(ring) * 0.18)
let speed = self.mode == .still ? 0.8 : 1.8
let phase = date.timeIntervalSinceReferenceDate * speed + Double(ring) * 0.9
return CGFloat(base + (sin(phase) * 0.035))
}
private func ringOpacity(_ ring: Int) -> Double {
switch self.mode {
case .still:
0.10 - (Double(ring) * 0.018)
default:
0.24 - (Double(ring) * 0.045)
}
}
}
private struct TalkProWaveform: View {
let mode: TalkProWaveformMode
let tint: Color
let barCount: Int
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
TimelineView(.periodic(from: .now, by: 1.0 / 24.0)) { timeline in
HStack(alignment: .center, spacing: 4) {
ForEach(0..<self.barCount, id: \.self) { index in
Capsule(style: .continuous)
.fill(self.tint.opacity(self.opacity(for: index)))
.frame(width: 4, height: self.height(for: index, date: timeline.date))
}
}
.frame(maxHeight: .infinity)
}
}
private func height(for index: Int, date: Date) -> CGFloat {
let minimum = 6.0
let maximum = 48.0
return CGFloat(minimum + ((maximum - minimum) * self.amplitude(for: index, date: date)))
}
private func opacity(for index: Int) -> Double {
switch self.mode {
case .still:
index == self.barCount / 2 ? 0.64 : 0.30
default:
0.82
}
}
private func amplitude(for index: Int, date: Date) -> Double {
if self.reduceMotion {
switch self.mode {
case let .level(level): return min(max(level, 0.10), 1.0)
case .inputSpeech: return 0.72
case .speaking: return 0.62
case .indeterminate: return 0.34
case .still: return 0.18
}
}
let t = date.timeIntervalSinceReferenceDate
let phase = Double(index) * 0.52
switch self.mode {
case let .level(level):
let clamped = min(max(level, 0), 1)
let shaped = 0.12 + (0.88 * clamped)
let variation = 0.72 + (0.28 * sin((t * 12.0) + phase))
return min(max(shaped * variation, 0.10), 1.0)
case .inputSpeech:
let primary = 0.5 + (0.5 * sin((t * 14.0) + phase))
let secondary = 0.5 + (0.5 * sin((t * 5.0) + (phase * 1.35)))
return min(max(0.16 + (0.60 * primary) + (0.24 * secondary), 0.14), 1.0)
case .speaking:
let wave = 0.5 + (0.5 * sin((t * 7.5) + phase))
let secondary = 0.5 + (0.5 * sin((t * 3.0) + (phase * 0.7)))
return min(max(0.18 + (0.58 * wave) + (0.24 * secondary), 0.12), 1.0)
case .indeterminate:
let center = (sin((t * 3.2) + phase) + 1) / 2
return 0.16 + (0.42 * center)
case .still:
return index == self.barCount / 2 ? 0.32 : 0.16
}
}
}

View File

@@ -17,6 +17,7 @@ private struct RelayGatewayPushRegistrationPayload: Encodable {
var topic: String
var environment: String
var distribution: String
var relayOrigin: String
var tokenDebugSuffix: String?
}
@@ -107,6 +108,7 @@ actor PushRegistrationManager {
topic: topic,
environment: self.buildConfig.apnsEnvironment.rawValue,
distribution: self.buildConfig.distribution.rawValue,
relayOrigin: relayOrigin,
tokenDebugSuffix: stored.tokenDebugSuffix))
}
@@ -138,6 +140,7 @@ actor PushRegistrationManager {
topic: topic,
environment: self.buildConfig.apnsEnvironment.rawValue,
distribution: self.buildConfig.distribution.rawValue,
relayOrigin: relayOrigin,
tokenDebugSuffix: registrationState.tokenDebugSuffix))
}

View File

@@ -36,6 +36,7 @@ struct RootTabs: View {
private enum AppTab: Hashable {
case control
case chat
case talk
case agent
case settings
}
@@ -53,6 +54,8 @@ struct RootTabs: View {
switch arguments[valueIndex].lowercased() {
case "chat":
return .chat
case "talk", "voice":
return .talk
case "agent", "agents":
return .agent
case "settings":
@@ -145,6 +148,14 @@ struct RootTabs: View {
.tabItem { Label("Chat", systemImage: "bubble.left.fill") }
.tag(AppTab.chat)
TalkProTab(openSettings: { self.selectedTab = .settings })
.tabItem {
Label(
"Talk",
systemImage: self.appModel.talkMode.isEnabled ? "waveform.circle.fill" : "waveform.circle")
}
.tag(AppTab.talk)
AgentProTab()
.tabItem { Label("Agent", systemImage: "person.2.fill") }
.tag(AppTab.agent)

View File

@@ -17,7 +17,7 @@ private func makeRealtimeAudioTapBlock(
inputSampleRate: inputSampleRate,
targetSampleRate: targetSampleRate)
guard !encoded.isEmpty else { return }
let timestampMs = ProcessInfo.processInfo.systemUptime * 1000
let timestampMs = (ProcessInfo.processInfo.systemUptime * 1000).rounded()
let rms = RealtimeTalkRelaySession.rmsLevel(buffer: buffer)
onAudio(encoded, timestampMs, rms)
}
@@ -125,15 +125,24 @@ final class RealtimeTalkRelaySession {
private var eventTask: Task<Void, Never>?
private var outputTask: Task<Void, Never>?
private var outputContinuation: AsyncThrowingStream<Data, Error>.Continuation?
private var outputIdleTask: Task<Void, Never>?
private var outputSessionId = 0
private var pendingOutputChunks: [Data] = []
private var pendingOutputDone = false
private var audioSender: RealtimeAudioSender?
private var isClosed = false
private var isOutputPlaying = false
private var outputStartedAtMs: Double?
private var outputPlaybackExpectedEndMs: Double = 0
private var lastBargeInAtMs: Double = 0
private var micLogFrameCount = 0
private var micLogByteCount = 0
private var micLogMaxRms: Float = 0
private var lastMicLogAtMs: Double = 0
private var suppressedEchoFrameCount = 0
private var suppressedEchoByteCount = 0
private var suppressedEchoMaxRms: Float = 0
private var lastSuppressedEchoLogAtMs: Double = 0
private var outputAudioChunkCount = 0
private var outputAudioByteCount = 0
@@ -168,7 +177,6 @@ final class RealtimeTalkRelaySession {
let eventStream = await self.gateway.subscribeServerEvents(bufferingNewest: 200)
self.startEventPump(stream: eventStream)
self.configureAudioContract(result.audio)
self.startOutputPlayback()
try self.startMicrophonePump()
self.onStatus("Listening (Realtime)")
} catch {
@@ -219,7 +227,6 @@ final class RealtimeTalkRelaySession {
func cancelOutput(reason: String = "user") {
self.stopOutputPlayback()
self.startOutputPlayback()
guard let relaySessionId else { return }
Task { [gateway] in
let payload: [String: Any] = [
@@ -306,12 +313,18 @@ final class RealtimeTalkRelaySession {
let data = Data(base64Encoded: base64)
else { return }
self.recordOutputAudioChunk(byteCount: data.count)
self.markOutputAudioStarted(nowMs: ProcessInfo.processInfo.systemUptime * 1000)
self.markOutputAudioStarted(byteCount: data.count, nowMs: ProcessInfo.processInfo.systemUptime * 1000)
self.onSpeakingChanged(true)
if self.outputContinuation == nil, self.outputTask != nil {
self.pendingOutputChunks.append(data)
return
}
self.ensureOutputPlaybackStarted()
self.outputContinuation?.yield(data)
case "audioDone":
self.finishOutputPlaybackStream()
case "clear":
self.stopOutputPlayback()
self.startOutputPlayback()
case "transcript":
self.handleTranscriptEvent(payload)
case "toolCall":
@@ -337,11 +350,16 @@ final class RealtimeTalkRelaySession {
"talk realtime audio: chunks=\(self.outputAudioChunkCount) bytes=\(self.outputAudioByteCount)")
}
private func markOutputAudioStarted(nowMs: Double) {
private func markOutputAudioStarted(byteCount: Int, nowMs: Double) {
if !self.isOutputPlaying {
self.outputStartedAtMs = nowMs
self.outputPlaybackExpectedEndMs = nowMs
}
self.isOutputPlaying = true
let bytesPerSecond = max(1, self.outputSampleRateHz * Double(MemoryLayout<Int16>.size))
let chunkDurationMs = (Double(byteCount) / bytesPerSecond) * 1000
self.outputPlaybackExpectedEndMs = max(nowMs, self.outputPlaybackExpectedEndMs) + chunkDurationMs
self.scheduleOutputPlaybackIdle(expectedEndMs: self.outputPlaybackExpectedEndMs)
}
private func handleInputLevelDuringOutput(_ rms: Float, timestampMs: Double) {
@@ -537,14 +555,25 @@ final class RealtimeTalkRelaySession {
{ [weak self, audioSender = self.audioSender] encoded, timestampMs, rms in
guard let audioSender else { return }
Task {
await MainActor.run { [weak self] in
self?.recordMicrophoneFrame(byteCount: encoded.count, rms: rms, timestampMs: timestampMs)
}
if rms >= Self.bargeInRmsThreshold {
await MainActor.run { [weak self] in
self?.handleInputLevelDuringOutput(rms, timestampMs: timestampMs)
let shouldSend = await MainActor.run { [weak self] in
guard let self, !self.isClosed else { return false }
self.recordMicrophoneFrame(byteCount: encoded.count, rms: rms, timestampMs: timestampMs)
self.refreshOutputPlaybackState(timestampMs: timestampMs)
if self.isOutputPlaying {
if self.shouldSuppressMicrophoneDuringOutput() {
self.recordSuppressedOutputEchoFrame(
byteCount: encoded.count,
rms: rms,
timestampMs: timestampMs)
return false
}
if rms >= Self.bargeInRmsThreshold {
self.handleInputLevelDuringOutput(rms, timestampMs: timestampMs)
}
}
return true
}
guard shouldSend else { return }
guard let message = await audioSender.send(encoded, timestampMs: timestampMs) else { return }
await MainActor.run { [weak self] in
guard let self, !self.isClosed else { return }
@@ -561,6 +590,13 @@ final class RealtimeTalkRelaySession {
try self.audioEngine.start()
}
private func shouldSuppressMicrophoneDuringOutput() -> Bool {
let outputs = AVAudioSession.sharedInstance().currentRoute.outputs
// Built-in speaker output bleeds into the microphone even in voiceChat mode; keep the
// realtime provider from treating its own speech as user input. Headsets keep barge-in.
return outputs.contains { $0.portType == .builtInSpeaker }
}
private func recordMicrophoneFrame(byteCount: Int, rms: Float, timestampMs: Double) {
guard !self.isClosed else { return }
self.micLogFrameCount += 1
@@ -576,13 +612,31 @@ final class RealtimeTalkRelaySession {
self.micLogMaxRms = 0
}
private func recordSuppressedOutputEchoFrame(byteCount: Int, rms: Float, timestampMs: Double) {
self.suppressedEchoFrameCount += 1
self.suppressedEchoByteCount += byteCount
self.suppressedEchoMaxRms = max(self.suppressedEchoMaxRms, rms)
guard timestampMs - self.lastSuppressedEchoLogAtMs >= 1000 else { return }
self.lastSuppressedEchoLogAtMs = timestampMs
let maxRms = String(format: "%.4f", Double(self.suppressedEchoMaxRms))
GatewayDiagnostics.log(
"talk realtime mic suppressed during output: "
+ "buffers=\(self.suppressedEchoFrameCount) "
+ "bytes=\(self.suppressedEchoByteCount) maxRms=\(maxRms)")
self.suppressedEchoFrameCount = 0
self.suppressedEchoByteCount = 0
self.suppressedEchoMaxRms = 0
}
private func stopMicrophonePump() {
self.audioEngine.inputNode.removeTap(onBus: 0)
self.audioEngine.stop()
}
private func startOutputPlayback() {
self.stopOutputPlayback()
private func ensureOutputPlaybackStarted() {
guard self.outputContinuation == nil, self.outputTask == nil else { return }
self.outputSessionId += 1
let sessionId = self.outputSessionId
let stream = AsyncThrowingStream<Data, Error> { continuation in
self.outputContinuation = continuation
}
@@ -590,28 +644,95 @@ final class RealtimeTalkRelaySession {
guard let self else { return }
let result = await self.pcmPlayer.play(stream: stream, sampleRate: self.outputSampleRateHz)
await MainActor.run {
guard self.outputSessionId == sessionId else { return }
self.outputTask = nil
self.outputContinuation = nil
if !result.finished, let interruptedAt = result.interruptedAt {
self.logger.info("realtime output interrupted at \(interruptedAt, privacy: .public)s")
}
self.markOutputPlaybackFinished()
self.startPendingOutputPlaybackIfNeeded()
}
}
}
private func markOutputPlaybackFinished() {
private func finishOutputPlaybackStream() {
guard let continuation = self.outputContinuation else {
if self.outputTask != nil, !self.pendingOutputChunks.isEmpty {
self.pendingOutputDone = true
}
return
}
continuation.finish()
self.outputContinuation = nil
}
private func startPendingOutputPlaybackIfNeeded() {
guard !self.pendingOutputChunks.isEmpty else {
self.pendingOutputDone = false
return
}
let chunks = self.pendingOutputChunks
let shouldFinish = self.pendingOutputDone
self.pendingOutputChunks = []
self.pendingOutputDone = false
self.ensureOutputPlaybackStarted()
for chunk in chunks {
self.markOutputAudioStarted(byteCount: chunk.count, nowMs: ProcessInfo.processInfo.systemUptime * 1000)
self.onSpeakingChanged(true)
self.outputContinuation?.yield(chunk)
}
if shouldFinish {
self.finishOutputPlaybackStream()
}
}
private func scheduleOutputPlaybackIdle(expectedEndMs: Double) {
self.outputIdleTask?.cancel()
let nowMs = ProcessInfo.processInfo.systemUptime * 1000
let idleDelayMs = max(350, expectedEndMs - nowMs + 500)
self.outputIdleTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: UInt64(idleDelayMs * 1_000_000))
guard !Task.isCancelled else { return }
await MainActor.run { [weak self] in
guard let self, !self.isClosed else { return }
let nowMs = ProcessInfo.processInfo.systemUptime * 1000
self.refreshOutputPlaybackState(timestampMs: nowMs, cancelIdleTask: false)
}
}
}
private func refreshOutputPlaybackState(timestampMs: Double, cancelIdleTask: Bool = true) {
guard self.isOutputPlaying else { return }
guard timestampMs >= self.outputPlaybackExpectedEndMs + 500 else { return }
self.markOutputPlaybackFinished(cancelIdleTask: cancelIdleTask)
}
private func markOutputPlaybackFinished(cancelIdleTask: Bool = true) {
if cancelIdleTask {
self.outputIdleTask?.cancel()
self.outputIdleTask = nil
}
self.isOutputPlaying = false
self.outputStartedAtMs = nil
self.outputPlaybackExpectedEndMs = 0
self.onSpeakingChanged(false)
}
private func stopOutputPlayback() {
self.outputSessionId += 1
self.outputContinuation?.finish()
self.outputContinuation = nil
self.outputTask?.cancel()
self.outputTask = nil
self.outputIdleTask?.cancel()
self.outputIdleTask = nil
self.pendingOutputChunks = []
self.pendingOutputDone = false
_ = self.pcmPlayer.stop()
self.isOutputPlaying = false
self.outputStartedAtMs = nil
self.outputPlaybackExpectedEndMs = 0
self.onSpeakingChanged(false)
}
@@ -684,7 +805,7 @@ final class RealtimeTalkRelaySession {
extension RealtimeTalkRelaySession {
func _test_markOutputAudioStarted(nowMs: Double) {
self.markOutputAudioStarted(nowMs: nowMs)
self.markOutputAudioStarted(byteCount: 4800, nowMs: nowMs)
}
func _test_markOutputPlaybackFinished() {

View File

@@ -1141,7 +1141,7 @@ final class TalkModeManager: NSObject {
})
self.realtimeRelaySession = relaySession
do {
try Self.configureAudioSession()
try Self.configureRealtimeAudioSession()
try await relaySession.start()
guard self.realtimeRelaySession === relaySession, self.isEnabled else {
relaySession.stop()

View File

@@ -13,6 +13,7 @@ Sources/Design/AgentProNodesDestination.swift
Sources/Design/AgentProTab.swift
Sources/Design/ChatProTab.swift
Sources/Design/CommandCenterTab.swift
Sources/Design/TalkProTab.swift
Sources/Design/OpenClawProComponents.swift
Sources/Design/OpenClawProScreens.swift
Sources/Design/SettingsProTab.swift

View File

@@ -361,6 +361,26 @@
}
}
},
"get_goal": {
"emoji": "🎯",
"title": "Get Goal",
"detailKeys": []
},
"create_goal": {
"emoji": "🎯",
"title": "Create Goal",
"detailKeys": [
"objective",
"token_budget"
]
},
"update_goal": {
"emoji": "🎯",
"title": "Update Goal",
"detailKeys": [
"status"
]
},
"update_plan": {
"emoji": "🗺️",
"title": "Update Plan",

View File

@@ -556,7 +556,7 @@ public struct MessageActionParams: Codable, Sendable {
sessionkey: String?,
sessionid: String?,
inboundturnkind: String? = nil,
agentid: String?,
agentid: String? = nil,
toolcontext: [String: AnyCodable]?,
idempotencykey: String)
{
@@ -617,7 +617,7 @@ public struct SendParams: Codable, Sendable {
gifplayback: Bool?,
channel: String?,
accountid: String?,
agentid: String?,
agentid: String? = nil,
replytoid: String?,
threadid: String?,
forcedocument: Bool?,
@@ -765,7 +765,7 @@ public struct AgentParams: Codable, Sendable {
public init(
message: String,
agentid: String?,
agentid: String? = nil,
provider: String?,
model: String?,
to: String?,
@@ -893,7 +893,7 @@ public struct AgentIdentityParams: Codable, Sendable {
public let sessionkey: String?
public init(
agentid: String?,
agentid: String? = nil,
sessionkey: String?)
{
self.agentid = agentid
@@ -1617,7 +1617,7 @@ public struct SessionsListParams: Codable, Sendable {
includelastmessage: Bool?,
label: String?,
spawnedby: String?,
agentid: String?,
agentid: String? = nil,
search: String?)
{
self.limit = limit
@@ -1741,7 +1741,7 @@ public struct SessionsResolveParams: Codable, Sendable {
key: String?,
sessionid: String?,
label: String?,
agentid: String?,
agentid: String? = nil,
spawnedby: String?,
includeglobal: Bool?,
includeunknown: Bool?)
@@ -1825,6 +1825,7 @@ public struct SessionOperationEvent: Codable, Sendable {
public let operation: String
public let phase: AnyCodable
public let sessionkey: String
public let agentid: String?
public let ts: Int
public let completed: Bool?
public let reason: String?
@@ -1834,6 +1835,7 @@ public struct SessionOperationEvent: Codable, Sendable {
operation: String,
phase: AnyCodable,
sessionkey: String,
agentid: String? = nil,
ts: Int,
completed: Bool?,
reason: String?)
@@ -1842,6 +1844,7 @@ public struct SessionOperationEvent: Codable, Sendable {
self.operation = operation
self.phase = phase
self.sessionkey = sessionkey
self.agentid = agentid
self.ts = ts
self.completed = completed
self.reason = reason
@@ -1852,6 +1855,7 @@ public struct SessionOperationEvent: Codable, Sendable {
case operation
case phase
case sessionkey = "sessionKey"
case agentid = "agentId"
case ts
case completed
case reason
@@ -1860,68 +1864,84 @@ public struct SessionOperationEvent: Codable, Sendable {
public struct SessionsCompactionListParams: Codable, Sendable {
public let key: String
public let agentid: String?
public init(
key: String)
key: String,
agentid: String? = nil)
{
self.key = key
self.agentid = agentid
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
}
}
public struct SessionsCompactionGetParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let checkpointid: String
public init(
key: String,
agentid: String? = nil,
checkpointid: String)
{
self.key = key
self.agentid = agentid
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case checkpointid = "checkpointId"
}
}
public struct SessionsCompactionBranchParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let checkpointid: String
public init(
key: String,
agentid: String? = nil,
checkpointid: String)
{
self.key = key
self.agentid = agentid
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case checkpointid = "checkpointId"
}
}
public struct SessionsCompactionRestoreParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let checkpointid: String
public init(
key: String,
agentid: String? = nil,
checkpointid: String)
{
self.key = key
self.agentid = agentid
self.checkpointid = checkpointid
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case checkpointid = "checkpointId"
}
}
@@ -2046,7 +2066,7 @@ public struct SessionsCreateParams: Codable, Sendable {
public init(
key: String?,
agentid: String?,
agentid: String? = nil,
label: String?,
model: String?,
parentsessionkey: String?,
@@ -2078,6 +2098,7 @@ public struct SessionsCreateParams: Codable, Sendable {
public struct SessionsSendParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let message: String
public let thinking: String?
public let attachments: [AnyCodable]?
@@ -2086,6 +2107,7 @@ public struct SessionsSendParams: Codable, Sendable {
public init(
key: String,
agentid: String? = nil,
message: String,
thinking: String?,
attachments: [AnyCodable]?,
@@ -2093,6 +2115,7 @@ public struct SessionsSendParams: Codable, Sendable {
idempotencykey: String?)
{
self.key = key
self.agentid = agentid
self.message = message
self.thinking = thinking
self.attachments = attachments
@@ -2102,6 +2125,7 @@ public struct SessionsSendParams: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case message
case thinking
case attachments
@@ -2112,29 +2136,37 @@ public struct SessionsSendParams: Codable, Sendable {
public struct SessionsMessagesSubscribeParams: Codable, Sendable {
public let key: String
public let agentid: String?
public init(
key: String)
key: String,
agentid: String? = nil)
{
self.key = key
self.agentid = agentid
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
}
}
public struct SessionsMessagesUnsubscribeParams: Codable, Sendable {
public let key: String
public let agentid: String?
public init(
key: String)
key: String,
agentid: String? = nil)
{
self.key = key
self.agentid = agentid
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
}
}
@@ -2162,6 +2194,7 @@ public struct SessionsAbortParams: Codable, Sendable {
public struct SessionsPatchParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let label: AnyCodable?
public let thinkinglevel: AnyCodable?
public let fastmode: AnyCodable?
@@ -2188,6 +2221,7 @@ public struct SessionsPatchParams: Codable, Sendable {
public init(
key: String,
agentid: String? = nil,
label: AnyCodable?,
thinkinglevel: AnyCodable?,
fastmode: AnyCodable?,
@@ -2213,6 +2247,7 @@ public struct SessionsPatchParams: Codable, Sendable {
groupactivation: AnyCodable?)
{
self.key = key
self.agentid = agentid
self.label = label
self.thinkinglevel = thinkinglevel
self.fastmode = fastmode
@@ -2240,6 +2275,7 @@ public struct SessionsPatchParams: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case label
case thinkinglevel = "thinkingLevel"
case fastmode = "fastMode"
@@ -2320,39 +2356,47 @@ public struct SessionsPluginPatchResult: Codable, Sendable {
public struct SessionsResetParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let reason: AnyCodable?
public init(
key: String,
agentid: String? = nil,
reason: AnyCodable?)
{
self.key = key
self.agentid = agentid
self.reason = reason
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case reason
}
}
public struct SessionsDeleteParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let deletetranscript: Bool?
public let emitlifecyclehooks: Bool?
public init(
key: String,
agentid: String? = nil,
deletetranscript: Bool?,
emitlifecyclehooks: Bool?)
{
self.key = key
self.agentid = agentid
self.deletetranscript = deletetranscript
self.emitlifecyclehooks = emitlifecyclehooks
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case deletetranscript = "deleteTranscript"
case emitlifecyclehooks = "emitLifecycleHooks"
}
@@ -2360,18 +2404,22 @@ public struct SessionsDeleteParams: Codable, Sendable {
public struct SessionsCompactParams: Codable, Sendable {
public let key: String
public let agentid: String?
public let maxlines: Int?
public init(
key: String,
agentid: String? = nil,
maxlines: Int?)
{
self.key = key
self.agentid = agentid
self.maxlines = maxlines
}
private enum CodingKeys: String, CodingKey {
case key
case agentid = "agentId"
case maxlines = "maxLines"
}
}
@@ -2463,7 +2511,7 @@ public struct TaskSummary: Codable, Sendable {
runtime: String?,
status: AnyCodable,
title: String?,
agentid: String?,
agentid: String? = nil,
sessionkey: String?,
childsessionkey: String?,
ownerkey: String?,
@@ -2537,7 +2585,7 @@ public struct TasksListParams: Codable, Sendable {
public init(
status: AnyCodable?,
agentid: String?,
agentid: String? = nil,
sessionkey: String?,
limit: Int?,
cursor: String?)
@@ -4727,7 +4775,7 @@ public struct CommandsListParams: Codable, Sendable {
public let includeargs: Bool?
public init(
agentid: String?,
agentid: String? = nil,
provider: String?,
scope: AnyCodable?,
includeargs: Bool?)
@@ -4764,7 +4812,7 @@ public struct SkillsStatusParams: Codable, Sendable {
public let agentid: String?
public init(
agentid: String?)
agentid: String? = nil)
{
self.agentid = agentid
}
@@ -4779,7 +4827,7 @@ public struct ToolsCatalogParams: Codable, Sendable {
public let includeplugins: Bool?
public init(
agentid: String?,
agentid: String? = nil,
includeplugins: Bool?)
{
self.agentid = agentid
@@ -4913,7 +4961,7 @@ public struct ToolsEffectiveParams: Codable, Sendable {
public let sessionkey: String
public init(
agentid: String?,
agentid: String? = nil,
sessionkey: String)
{
self.agentid = agentid
@@ -5058,7 +5106,7 @@ public struct ToolsInvokeParams: Codable, Sendable {
name: String,
args: [String: AnyCodable]?,
sessionkey: String?,
agentid: String?,
agentid: String? = nil,
confirm: Bool?,
idempotencykey: String?)
{
@@ -5232,7 +5280,7 @@ public struct SkillsSecurityVerdictsParams: Codable, Sendable {
public let agentid: String?
public init(
agentid: String?)
agentid: String? = nil)
{
self.agentid = agentid
}
@@ -5265,7 +5313,7 @@ public struct SkillsSkillCardParams: Codable, Sendable {
public let skillkey: String
public init(
agentid: String?,
agentid: String? = nil,
skillkey: String)
{
self.agentid = agentid
@@ -5402,7 +5450,7 @@ public struct CronJob: Codable, Sendable {
public init(
id: String,
agentid: String?,
agentid: String? = nil,
sessionkey: String?,
name: String,
description: String?,
@@ -5478,7 +5526,7 @@ public struct CronListParams: Codable, Sendable {
lastrunstatus: AnyCodable?,
sortby: AnyCodable?,
sortdir: AnyCodable?,
agentid: String?)
agentid: String? = nil)
{
self.includedisabled = includedisabled
self.limit = limit
@@ -5524,7 +5572,7 @@ public struct CronAddParams: Codable, Sendable {
public init(
name: String,
agentid: AnyCodable?,
agentid: AnyCodable? = nil,
sessionkey: AnyCodable?,
description: String?,
enabled: Bool?,
@@ -5912,7 +5960,7 @@ public struct ExecApprovalRequestParams: Codable, Sendable {
ask: AnyCodable?,
warningtext: AnyCodable?,
commandspans: [[String: AnyCodable]]?,
agentid: AnyCodable?,
agentid: AnyCodable? = nil,
resolvedpath: AnyCodable?,
sessionkey: AnyCodable?,
turnsourcechannel: AnyCodable?,
@@ -6019,7 +6067,7 @@ public struct PluginApprovalRequestParams: Codable, Sendable {
toolname: String?,
toolcallid: String?,
alloweddecisions: [String]?,
agentid: String?,
agentid: String? = nil,
sessionkey: String?,
turnsourcechannel: String?,
turnsourceto: String?,
@@ -6480,21 +6528,25 @@ public struct DevicePairResolvedEvent: Codable, Sendable {
public struct ChatHistoryParams: Codable, Sendable {
public let sessionkey: String
public let agentid: String?
public let limit: Int?
public let maxchars: Int?
public init(
sessionkey: String,
agentid: String? = nil,
limit: Int?,
maxchars: Int?)
{
self.sessionkey = sessionkey
self.agentid = agentid
self.limit = limit
self.maxchars = maxchars
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case agentid = "agentId"
case limit
case maxchars = "maxChars"
}
@@ -6502,6 +6554,7 @@ public struct ChatHistoryParams: Codable, Sendable {
public struct ChatSendParams: Codable, Sendable {
public let sessionkey: String
public let agentid: String?
public let sessionid: String?
public let message: String
public let thinking: String?
@@ -6519,6 +6572,7 @@ public struct ChatSendParams: Codable, Sendable {
public init(
sessionkey: String,
agentid: String? = nil,
sessionid: String?,
message: String,
thinking: String?,
@@ -6535,6 +6589,7 @@ public struct ChatSendParams: Codable, Sendable {
idempotencykey: String)
{
self.sessionkey = sessionkey
self.agentid = agentid
self.sessionid = sessionid
self.message = message
self.thinking = thinking
@@ -6553,6 +6608,7 @@ public struct ChatSendParams: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case agentid = "agentId"
case sessionid = "sessionId"
case message
case thinking
@@ -6572,39 +6628,47 @@ public struct ChatSendParams: Codable, Sendable {
public struct ChatAbortParams: Codable, Sendable {
public let sessionkey: String
public let agentid: String?
public let runid: String?
public init(
sessionkey: String,
agentid: String? = nil,
runid: String?)
{
self.sessionkey = sessionkey
self.agentid = agentid
self.runid = runid
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case agentid = "agentId"
case runid = "runId"
}
}
public struct ChatInjectParams: Codable, Sendable {
public let sessionkey: String
public let agentid: String?
public let message: String
public let label: String?
public init(
sessionkey: String,
agentid: String? = nil,
message: String,
label: String?)
{
self.sessionkey = sessionkey
self.agentid = agentid
self.message = message
self.label = label
}
private enum CodingKeys: String, CodingKey {
case sessionkey = "sessionKey"
case agentid = "agentId"
case message
case label
}
@@ -6613,6 +6677,7 @@ public struct ChatInjectParams: Codable, Sendable {
public struct ChatDeltaEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let agentid: String?
public let spawnedby: String?
public let seq: Int
public let state: String
@@ -6624,6 +6689,7 @@ public struct ChatDeltaEvent: Codable, Sendable {
public init(
runid: String,
sessionkey: String,
agentid: String? = nil,
spawnedby: String?,
seq: Int,
state: String,
@@ -6634,6 +6700,7 @@ public struct ChatDeltaEvent: Codable, Sendable {
{
self.runid = runid
self.sessionkey = sessionkey
self.agentid = agentid
self.spawnedby = spawnedby
self.seq = seq
self.state = state
@@ -6646,6 +6713,7 @@ public struct ChatDeltaEvent: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case sessionkey = "sessionKey"
case agentid = "agentId"
case spawnedby = "spawnedBy"
case seq
case state
@@ -6659,6 +6727,7 @@ public struct ChatDeltaEvent: Codable, Sendable {
public struct ChatFinalEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let agentid: String?
public let spawnedby: String?
public let seq: Int
public let state: String
@@ -6669,6 +6738,7 @@ public struct ChatFinalEvent: Codable, Sendable {
public init(
runid: String,
sessionkey: String,
agentid: String? = nil,
spawnedby: String?,
seq: Int,
state: String,
@@ -6678,6 +6748,7 @@ public struct ChatFinalEvent: Codable, Sendable {
{
self.runid = runid
self.sessionkey = sessionkey
self.agentid = agentid
self.spawnedby = spawnedby
self.seq = seq
self.state = state
@@ -6689,6 +6760,7 @@ public struct ChatFinalEvent: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case sessionkey = "sessionKey"
case agentid = "agentId"
case spawnedby = "spawnedBy"
case seq
case state
@@ -6701,6 +6773,7 @@ public struct ChatFinalEvent: Codable, Sendable {
public struct ChatAbortedEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let agentid: String?
public let spawnedby: String?
public let seq: Int
public let state: String
@@ -6710,6 +6783,7 @@ public struct ChatAbortedEvent: Codable, Sendable {
public init(
runid: String,
sessionkey: String,
agentid: String? = nil,
spawnedby: String?,
seq: Int,
state: String,
@@ -6718,6 +6792,7 @@ public struct ChatAbortedEvent: Codable, Sendable {
{
self.runid = runid
self.sessionkey = sessionkey
self.agentid = agentid
self.spawnedby = spawnedby
self.seq = seq
self.state = state
@@ -6728,6 +6803,7 @@ public struct ChatAbortedEvent: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case sessionkey = "sessionKey"
case agentid = "agentId"
case spawnedby = "spawnedBy"
case seq
case state
@@ -6739,6 +6815,7 @@ public struct ChatAbortedEvent: Codable, Sendable {
public struct ChatErrorEvent: Codable, Sendable {
public let runid: String
public let sessionkey: String
public let agentid: String?
public let spawnedby: String?
public let seq: Int
public let state: String
@@ -6751,6 +6828,7 @@ public struct ChatErrorEvent: Codable, Sendable {
public init(
runid: String,
sessionkey: String,
agentid: String? = nil,
spawnedby: String?,
seq: Int,
state: String,
@@ -6762,6 +6840,7 @@ public struct ChatErrorEvent: Codable, Sendable {
{
self.runid = runid
self.sessionkey = sessionkey
self.agentid = agentid
self.spawnedby = spawnedby
self.seq = seq
self.state = state
@@ -6775,6 +6854,7 @@ public struct ChatErrorEvent: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case sessionkey = "sessionKey"
case agentid = "agentId"
case spawnedby = "spawnedBy"
case seq
case state

View File

@@ -1,4 +1,4 @@
c80dea63b0a3786c8999d06aae62c110786f440b4d6748f9838577aaa2816971 config-baseline.json
948323a1507817b6580ed976f9f9449239008f40283cc7e6005148ecf0ca4582 config-baseline.core.json
f833ffca6bd88162f062bbea4f0eede783373f46674ebbfc3a390c80353930a2 config-baseline.channel.json
bc38b58b67132401a030b3b3a77efdb6c88f207ea1fab9abcb4599e1f9552dda config-baseline.plugin.json
ac5e91a6adaf02491d2ff6b983f054c813972da3bf79db68cd1d10887a22c594 config-baseline.json
023e3b85ee79e85f90257e65a1376b1212cf534b6a9cff4b4388c9092e846549 config-baseline.core.json
a9102c0611b8170fac37853cc31771810f31757a9e3b2c6796bbd9625f9b9206 config-baseline.channel.json
2f018852d9682871dd22f0920cafc8994a6c0952e8101229210efa6103ae9536 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
59de21361cab0622926ad313caf3f8dc43c28d420a82ba060680ecc30c472453 plugin-sdk-api-baseline.json
05adee9037669db4e834d1a0ca9705d5d94df770083862ab149d2f3e559010d2 plugin-sdk-api-baseline.jsonl
49a138a9743063067b983c4dd27d047572aef0764c0e5f87a98d91f43d4f8213 plugin-sdk-api-baseline.json
cd7ea2f2b4c1d1d073c3077410d44270244e778f33197567f4127a946cc0f7f7 plugin-sdk-api-baseline.jsonl

View File

@@ -102,7 +102,7 @@ Not every agent run creates a task. Heartbeat turns and normal interactive chat
<Accordion title="Notify defaults for cron and media">
Main-session cron tasks use `silent` notify policy by default - they create records for tracking but do not generate notifications. Isolated cron tasks also default to `silent` but are more visible because they run in their own session.
Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. Generated-media completion events require message-tool delivery: the agent must send the finished media with the `message` tool, then reply `NO_REPLY`. If the requester session is no longer active or its active wake fails, and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target.
Session-backed `image_generate`, `music_generate`, and `video_generate` runs also use `silent` notify policy. They still create task records, but completion is handed back to the original agent session as an internal wake so the agent can write the follow-up message and attach the finished media itself. The requester agent follows its normal visible-reply contract: automatic final reply when configured, or `message(action="send")` plus `NO_REPLY` when the session requires message-tool replies. If the requester session is no longer active or its active wake fails, and the completion agent misses some or all generated media, OpenClaw sends an idempotent direct fallback with only the missing media to the original channel target.
</Accordion>
<Accordion title="Concurrent media-generation guardrail">

View File

@@ -43,6 +43,7 @@ Notes:
- Local mode uses the embedded agent runtime directly. Most local tools work, but Gateway-only features are unavailable.
- Local mode adds `/auth [provider]` inside the TUI command surface.
- Plugin approval gates still apply in local mode. Tools that require approval prompt for a decision in the terminal; nothing is silently auto-approved because the Gateway is not involved.
- Session [goals](/tools/goal) appear in the footer and can be managed with `/goal`.
## Examples
@@ -87,3 +88,4 @@ rerun `openclaw config validate`. See [TUI](/web/tui) and [Config](/cli/config).
- [CLI reference](/cli)
- [TUI](/web/tui)
- [Goal](/tools/goal)

View File

@@ -40,7 +40,7 @@ There are two runtime families:
model, execute through Claude CLI." `claude-cli` is not an embedded harness id
and must not be passed to AgentHarness selection.
The `copilot` harness is a separate, opt-in plugin harness for the
The `copilot` harness is a separate, opt-in external plugin harness for the
GitHub Copilot CLI; see [GitHub Copilot agent runtime](/plugins/copilot)
for the user-facing decision between PI, Codex, and GitHub Copilot agent runtime.
@@ -207,7 +207,7 @@ If `openclaw doctor` warns that the `codex` plugin is enabled while
## GitHub Copilot agent runtime
The bundled `copilot` extension registers an opt-in `copilot` runtime
The external `@openclaw/copilot` plugin registers an opt-in `copilot` runtime
backed by the GitHub Copilot CLI (`@github/copilot-sdk`). It claims the
canonical subscription `github-copilot` provider and is **never** selected by
`auto`. Opt in per-model or per-provider via `agentRuntime.id`:

View File

@@ -30,7 +30,7 @@ Treat them differently from normal config:
## Local model lean mode
`agents.defaults.experimental.localModelLean: true` is a pressure-release valve for weaker local-model setups. When it is on, OpenClaw drops three default tools — `browser`, `cron`, and `message` — from the model-visible tool surface for every turn. When Code Mode or Tool Search is enabled, those tools can still stay in the hidden catalog behind the compact controls. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
`agents.defaults.experimental.localModelLean: true` is a pressure-release valve for weaker local-model setups. When it is on, OpenClaw drops three default tools — `browser`, `cron`, and `message` — from the agent's tool surface for every turn. Nothing else changes. Use `agents.list[].experimental.localModelLean` to enable or disable the same behavior for one configured agent.
### Why these three tools
@@ -40,7 +40,7 @@ These three tools have the largest descriptions and the most parameter shapes in
- The model picking the right tool vs. emitting malformed tool calls because there are too many similar-looking schemas.
- The Chat Completions adapter staying inside the server's structured-output limits vs. tripping a 400 on tool-call payload size.
Removing them does not silently rewire OpenClaw — it just makes the visible tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available. With Code Mode or Tool Search, the compact control can still search for and call hidden catalog tools that policy allowed for the run.
Removing them does not silently rewire OpenClaw — it just makes the tool list shorter. The model still has `read`, `write`, `edit`, `exec`, `apply_patch`, web search/fetch (when configured), memory, and session/agent tools available.
### When to turn it on
@@ -94,7 +94,7 @@ Restart the Gateway after changing the flag, then confirm the trimmed tool list
openclaw status --deep
```
The deep status output lists the active model-visible agent tools; `browser`, `cron`, and `message` should be absent when lean mode is on. If Code Mode or Tool Search is enabled, they may still be available through the hidden catalog.
The deep status output lists the active agent tools; `browser`, `cron`, and `message` should be absent when lean mode is on.
## Experimental does not mean hidden

View File

@@ -23,7 +23,7 @@ sidebarTitle: "Models CLI"
</Card>
</CardGroup>
Model refs choose a provider and model. They do not usually choose the low-level agent runtime. OpenAI agent refs are the main exception: `openai/gpt-5.5` runs through the Codex app-server runtime by default on the official OpenAI provider. Subscription Copilot refs (`github-copilot/*`) can additionally be opted into the bundled GitHub Copilot agent runtime — that path stays explicit (no `auto` fallback). Explicit runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, the `openai/gpt-*` ref does not imply API-key billing; auth can come from a Codex account or `openai-codex` auth profile. See [Agent runtimes](/concepts/agent-runtimes) and [GitHub Copilot agent runtime](/plugins/copilot).
Model refs choose a provider and model. They do not usually choose the low-level agent runtime. OpenAI agent refs are the main exception: `openai/gpt-5.5` runs through the Codex app-server runtime by default on the official OpenAI provider. Subscription Copilot refs (`github-copilot/*`) can additionally be opted into the external GitHub Copilot agent runtime plugin — that path stays explicit (no `auto` fallback). Explicit runtime overrides belong on provider/model policy, not on the whole agent or session. In Codex runtime mode, the `openai/gpt-*` ref does not imply API-key billing; auth can come from a Codex account or `openai-codex` auth profile. See [Agent runtimes](/concepts/agent-runtimes) and [GitHub Copilot agent runtime](/plugins/copilot).
## How model selection works

View File

@@ -266,6 +266,10 @@ The doctor checks Convex broker env, validates endpoint settings, and verifies a
Live transport lanes share one contract instead of each inventing their own scenario list shape. `qa-channel` is the broad synthetic product-behavior suite and is not part of the live transport coverage matrix.
Live transport runners should import the shared scenario ids, baseline
coverage helpers, and scenario-selection helper from
`openclaw/plugin-sdk/qa-live-transport-scenarios`.
| Lane | Canary | Mention gating | Bot-to-bot | Allowlist block | Top-level reply | Restart resume | Thread follow-up | Thread isolation | Reaction observation | Help command | Native command registration |
| -------- | ------ | -------------- | ---------- | --------------- | --------------- | -------------- | ---------------- | ---------------- | -------------------- | ------------ | --------------------------- |
| Matrix | x | x | x | x | x | x | x | x | x | | |

View File

@@ -1333,6 +1333,7 @@
"group": "Agent coordination",
"pages": [
"tools/agent-send",
"tools/goal",
"tools/steer",
"tools/subagents",
"tools/acp-agents",

View File

@@ -617,7 +617,7 @@ Periodic heartbeat runs.
- `every`: duration string (ms/s/m/h). Default: `30m` (API-key auth) or `1h` (OAuth auth). Set to `0m` to disable.
- `includeSystemPromptSection`: when false, omits the Heartbeat section from the system prompt and skips `HEARTBEAT.md` injection into bootstrap context. Default: `true`.
- `suppressToolErrorWarnings`: when true, suppresses tool error warning payloads during heartbeat runs.
- `timeoutSeconds`: maximum time in seconds allowed for a heartbeat agent turn before it is aborted. Leave unset to use `agents.defaults.timeoutSeconds`.
- `timeoutSeconds`: maximum time in seconds allowed for a heartbeat agent turn before it is aborted. Leave unset to use `agents.defaults.timeoutSeconds` when set, otherwise the heartbeat cadence capped at 600 seconds.
- `directPolicy`: direct/DM delivery policy. `allow` (default) permits direct-target delivery. `block` suppresses direct-target delivery and emits `reason=dm-blocked`.
- `lightContext`: when true, heartbeat runs use lightweight bootstrap context and keep only `HEARTBEAT.md` from workspace bootstrap files.
- `isolatedSession`: when true, each heartbeat runs in a fresh session with no prior conversation history. Same isolation pattern as cron `sessionTarget: "isolated"`. Reduces per-heartbeat token cost from ~100K to ~2-5K tokens.

View File

@@ -386,12 +386,13 @@ Controls inline attachment support for `sessions_spawn`.
<AccordionGroup>
<Accordion title="Attachment notes">
- Attachments are only supported for `runtime: "subagent"`. ACP runtime rejects them.
- Files are materialized into the child workspace at `.openclaw/attachments/<uuid>/` with a `.manifest.json`.
- Attachments require `enabled: true`.
- Subagent attachments are materialized into the child workspace at `.openclaw/attachments/<uuid>/` with a `.manifest.json`.
- ACP attachments are image-only and forwarded inline to the ACP runtime after the same file count, per-file byte, and total byte limits pass.
- Attachment content is automatically redacted from transcript persistence.
- Base64 inputs are validated with strict alphabet/padding checks and a pre-decode size guard.
- File permissions are `0700` for directories and `0600` for files.
- Cleanup follows the `cleanup` policy: `delete` always removes attachments; `keep` retains them only when `retainOnSessionKeep: true`.
- Subagent attachment file permissions are `0700` for directories and `0600` for files.
- Subagent cleanup follows the `cleanup` policy: `delete` always removes attachments; `keep` retains them only when `retainOnSessionKeep: true`.
</Accordion>
</AccordionGroup>

View File

@@ -337,9 +337,9 @@ candidate contains redacted secret placeholders such as `***`.
</Accordion>
<Accordion title="Enable relay-backed push for official iOS builds">
Relay-backed push is configured in `openclaw.json`.
Relay-backed push uses the hosted OpenClaw relay by default: `https://ios-push-relay.openclaw.ai`.
Set this in gateway config:
To use a custom relay, set this in gateway config:
```json5
{
@@ -373,8 +373,8 @@ candidate contains redacted secret placeholders such as `***`.
End-to-end flow:
1. Install an official/TestFlight iOS build that was compiled with the same relay base URL.
2. Configure `gateway.push.apns.relay.baseUrl` on the gateway.
1. Install an official/TestFlight iOS build.
2. Optional: configure `gateway.push.apns.relay.baseUrl` on the gateway only when using a custom relay deployment.
3. Pair the iOS app to the gateway and let both node and operator sessions connect.
4. The iOS app fetches the gateway identity, registers with the relay using App Attest plus the app receipt, and then publishes the relay-backed `push.apns.register` payload to the paired gateway.
5. The gateway stores the relay handle and send grant, then uses them for `push.test`, wake nudges, and reconnect wakes.
@@ -387,6 +387,7 @@ candidate contains redacted secret placeholders such as `***`.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` and `OPENCLAW_APNS_RELAY_TIMEOUT_MS` still work as temporary env overrides.
- Custom gateway relay URLs must match the relay base URL baked into the official/TestFlight iOS build.
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true` remains a loopback-only development escape hatch; do not persist HTTP relay URLs in config.
See [iOS App](/platforms/ios#relay-backed-push-for-official-builds) for the end-to-end flow and [Authentication and trust flow](/platforms/ios#authentication-and-trust-flow) for the relay security model.

View File

@@ -382,7 +382,7 @@ That stages grounded durable candidates into the short-term dreaming store while
</Accordion>
<Accordion title="3c. Session lock cleanup">
Doctor scans every agent session directory for stale write-lock files — files left behind when a session exited abnormally. For each lock file found it reports: the path, PID, whether the PID is still alive, lock age, and whether it is considered stale (dead PID, older than 30 minutes, or a live PID that can be proven to belong to a non-OpenClaw process). In `--fix` / `--repair` mode it removes stale lock files automatically; otherwise it prints a note and instructs you to rerun with `--fix`.
Doctor scans every agent session directory for stale write-lock files — files left behind when a session exited abnormally. For each lock file found it reports: the path, PID, whether the PID is still alive, lock age, and whether it is considered stale (dead PID, malformed owner metadata, older than 30 minutes, or a live PID that can be proven to belong to a non-OpenClaw process). In `--fix` / `--repair` mode it removes locks with dead, orphaned, recycled, malformed-old, or non-OpenClaw owners automatically. Old locks that are still owned by a live OpenClaw process are reported but left in place so doctor does not cut off an active transcript writer.
</Accordion>
<Accordion title="3d. Session transcript branch repair">
Doctor scans agent session JSONL files for the duplicated branch shape created by the 2026.4.24 prompt transcript rewrite bug: an abandoned user turn with OpenClaw internal runtime context plus an active sibling containing the same visible user prompt. In `--fix` / `--repair` mode, doctor backs up each affected file next to the original and rewrites the transcript to the active branch so gateway history and memory readers no longer see duplicate turns.

View File

@@ -63,6 +63,7 @@ Example config:
- Interval: `30m` (or `1h` when Anthropic OAuth/token auth is the detected auth mode, including Claude CLI reuse). Set `agents.defaults.heartbeat.every` or per-agent `agents.list[].heartbeat.every`; use `0m` to disable.
- Prompt body (configurable via `agents.defaults.heartbeat.prompt`): `Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.`
- Timeout: unset heartbeat turns use `agents.defaults.timeoutSeconds` when set. Otherwise, they use the heartbeat cadence capped at 600 seconds. Set `agents.defaults.heartbeat.timeoutSeconds` or per-agent `agents.list[].heartbeat.timeoutSeconds` for longer heartbeat work.
- The heartbeat prompt is sent **verbatim** as the user message. The system prompt includes a "Heartbeat" section only when heartbeats are enabled for the default agent, and the run is flagged internally.
- When heartbeats are disabled with `0m`, normal runs also omit `HEARTBEAT.md` from bootstrap context so the model does not see heartbeat-only instructions.
- Active hours (`heartbeat.activeHours`) are checked in the configured timezone. Outside the window, heartbeats are skipped until the next tick inside the window.
@@ -274,6 +275,10 @@ Use `accountId` to target a specific account on multi-account channels like Tele
<ParamField path="suppressToolErrorWarnings" type="boolean">
When true, suppresses tool error warning payloads during heartbeat runs.
</ParamField>
<ParamField path="timeoutSeconds" type="number" default="global timeout or min(every, 600)">
Maximum seconds allowed for a heartbeat agent turn before it is aborted. Leave unset to use `agents.defaults.timeoutSeconds` when set, otherwise the heartbeat cadence capped at 600 seconds.
</ParamField>
<ParamField path="activeHours" type="object">
Restricts heartbeat runs to a time window. Object with `start` (HH:MM, inclusive; use `00:00` for start-of-day), `end` (HH:MM exclusive; `24:00` allowed for end-of-day), and optional `timezone`.

View File

@@ -315,7 +315,7 @@ If the model loads cleanly but full agent turns misbehave, work top-down — con
openclaw infer model run --gateway --model <provider/model> --prompt "Reply with exactly: pong" --json
```
3. **Try lean mode.** If both probes pass but real agent turns fail with malformed tool calls or oversized prompts, enable `agents.defaults.experimental.localModelLean: true`. It drops the three heaviest default tools (`browser`, `cron`, `message`) from the visible model surface so the prompt shape is smaller and less brittle. When Code Mode or Tool Search is enabled, those tools can still sit behind the compact catalog controls. See [Experimental Features → Local model lean mode](/concepts/experimental-features#local-model-lean-mode) for the full explanation, when to use it, and how to confirm it is on.
3. **Try lean mode.** If both probes pass but real agent turns fail with malformed tool calls or oversized prompts, enable `agents.defaults.experimental.localModelLean: true`. It drops the three heaviest default tools (`browser`, `cron`, `message`) so the prompt shape is smaller and less brittle. See [Experimental Features → Local model lean mode](/concepts/experimental-features#local-model-lean-mode) for the full explanation, when to use it, and how to confirm it is on.
4. **Disable tools entirely as a last resort.** If lean mode is not enough, set `models.providers.<provider>.models[].compat.supportsTools: false` for that model entry. The agent will then operate without tool calls on that model.

View File

@@ -183,6 +183,13 @@ Define providers under `secrets.providers`:
passEnv: ["PATH", "VAULT_ADDR"],
jsonOnly: true,
},
"team-secrets": {
source: "exec",
pluginIntegration: {
pluginId: "acme-secrets",
integrationId: "secret-store",
},
},
},
defaults: {
env: "default",
@@ -219,6 +226,11 @@ Define providers under `secrets.providers`:
- Pair `allowSymlinkCommand` with `trustedDirs` for package-manager paths (for example `["/opt/homebrew"]`).
- Supports timeout, no-output timeout, output byte limits, env allowlist, and trusted dirs.
- Windows fail-closed note: if ACL verification is unavailable for the command path, resolution fails. For trusted paths only, set `allowInsecurePath: true` on that provider to bypass path security checks.
- Plugin-managed exec providers can use `pluginIntegration` instead of
copied `command`/`args`. OpenClaw resolves the current command details
from the installed plugin manifest during startup/reload. If the plugin is
disabled, removed, untrusted, or no longer declares the integration,
active SecretRefs using that provider fail closed.
Request payload (stdin):

View File

@@ -75,7 +75,9 @@ openclaw gateway call node.list --params "{}"
Official distributed iOS builds use the external push relay instead of publishing the raw APNs
token to the gateway.
Gateway-side requirement:
By default, official/TestFlight builds and gateways use the hosted relay at `https://ios-push-relay.openclaw.ai`.
Custom relay deployments can override the gateway relay URL:
```json5
{
@@ -98,7 +100,7 @@ How the flow works:
- The iOS app fetches the paired gateway identity and includes it in relay registration, so the relay-backed registration is delegated to that specific gateway.
- The app forwards that relay-backed registration to the paired gateway with `push.apns.register`.
- The gateway uses that stored relay handle for `push.test`, background wakes, and wake nudges.
- The gateway relay base URL must match the relay URL baked into the official/TestFlight iOS build.
- Custom gateway relay URLs must match the relay URL baked into the official/TestFlight iOS build.
- If the app later connects to a different gateway or a build with a different relay base URL, it refreshes the relay registration instead of reusing the old binding.
What the gateway does **not** need for this path:
@@ -109,7 +111,7 @@ What the gateway does **not** need for this path:
Expected operator flow:
1. Install the official/TestFlight iOS build.
2. Set `gateway.push.apns.relay.baseUrl` on the gateway.
2. Optional: set `gateway.push.apns.relay.baseUrl` on the gateway only when using a custom relay deployment.
3. Pair the app to the gateway and let it finish connecting.
4. The app publishes `push.apns.register` automatically after it has an APNs token, the operator session is connected, and relay registration succeeds.
5. After that, `push.test`, reconnect wakes, and wake nudges can use the stored relay-backed registration.
@@ -128,6 +130,7 @@ compatible but does not count as a durable last-seen update.
Compatibility note:
- `OPENCLAW_APNS_RELAY_BASE_URL` still works as a temporary env override for the gateway.
- `OPENCLAW_PUSH_RELAY_BASE_URL` still works as a temporary env override for official/TestFlight iOS builds.
## Authentication and trust flow

View File

@@ -85,25 +85,25 @@ For an already-running app-server, use WebSocket transport:
Supported `appServer` fields:
| Field | Default | Meaning |
| --------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. |
| `postToolRawAssistantCompletionIdleTimeoutMs` | unset | Completion-idle guard used after a tool handoff when Codex emits raw assistant completion or progress but does not send `turn/completed`. Defaults to the assistant completion idle timeout when unset. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start, resume, and turn. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start and resume. Active OpenClaw sandboxes narrow `danger-full-access` turns to Codex `workspace-write`; the turn network flag follows OpenClaw sandbox egress. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed. |
| `defaultWorkspaceDir` | current process directory | Workspace used by `/codex bind` when `--cwd` is omitted. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, and `null` clears the override. Legacy `"fast"` is accepted as `"priority"`. |
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
| Field | Default | Meaning |
| --------------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. |
| `postToolRawAssistantCompletionIdleTimeoutMs` | `300000` | Completion-idle guard used after a tool handoff when Codex emits raw assistant completion or progress but does not send `turn/completed`. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start, resume, and turn. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start and resume. Active OpenClaw sandboxes narrow `danger-full-access` turns to Codex `workspace-write`; the turn network flag follows OpenClaw sandbox egress. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed. |
| `defaultWorkspaceDir` | current process directory | Workspace used by `/codex bind` when `--cwd` is omitted. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, and `null` clears the override. Legacy `"fast"` is accepted as `"priority"`. |
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
The plugin blocks older or unversioned app-server handshakes. Codex app-server
must report stable version `0.125.0` or newer.
@@ -337,10 +337,15 @@ Codex then goes quiet without `turn/completed`, OpenClaw best-effort interrupts
the native turn and releases the session lane. Post-tool raw assistant progress
keeps waiting for `turn/completed` while a completion-idle guard stays armed; the
guard uses `appServer.postToolRawAssistantCompletionIdleTimeoutMs` when
configured and falls back to the assistant completion idle timeout otherwise.
Timeout diagnostics include the last app-server notification method and, for raw
assistant response items, the item type, role, id, and a bounded assistant text
preview.
configured and defaults to five minutes otherwise. Replay-safe stdio app-server
failures, including turn-completion idle timeouts without assistant, tool,
active-item, or side-effect evidence, are retried once on a fresh app-server
attempt. Unsafe timeouts still retire the stuck app-server client and release
the OpenClaw session lane. They also clear the stale native thread binding and
surface a recoverable timeout message for user or maintainer judgment instead of
being replayed automatically. Timeout diagnostics include the last
app-server notification method and, for raw assistant response items, the item
type, role, id, and a bounded assistant text preview.
## Model discovery

View File

@@ -525,25 +525,25 @@ Supported top-level Codex plugin fields:
Supported `appServer` fields:
| Field | Default | Meaning |
| --------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. |
| `codeModeOnly` | `false` | Opt into Codex's code-mode-only tool surface. OpenClaw dynamic tools remain registered with Codex so nested `tools.*` calls return through the app-server `item/tool/call` bridge. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
| `postToolRawAssistantCompletionIdleTimeoutMs` | unset | Completion-idle guard used after a tool handoff when Codex emits raw assistant completion or progress but does not send `turn/completed`. Defaults to the assistant completion idle timeout when unset. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. When an OpenClaw sandbox is active, `danger-full-access` turns use Codex `workspace-write` with network access derived from the OpenClaw sandbox egress setting. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. |
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
| Field | Default | Meaning |
| --------------------------------------------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. OpenClaw keeps per-agent `CODEX_HOME` and inherited `HOME` for local launches. |
| `codeModeOnly` | `false` | Opt into Codex's code-mode-only tool surface. OpenClaw dynamic tools remain registered with Codex so nested `tools.*` calls return through the app-server `item/tool/call` bridge. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after Codex accepts a turn or after a turn-scoped app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
| `postToolRawAssistantCompletionIdleTimeoutMs` | `300000` | Completion-idle guard used after a tool handoff when Codex emits raw assistant completion or progress but does not send `turn/completed`. Use this for trusted or heavy workloads where post-tool synthesis can legitimately stay quiet longer than the final assistant release budget. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. When an OpenClaw sandbox is active, `danger-full-access` turns use Codex `workspace-write` with network access derived from the OpenClaw sandbox egress setting. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. |
| `experimental.sandboxExecServer` | `false` | Preview opt-in that registers an OpenClaw sandbox-backed Codex environment with Codex app-server 0.132.0 or newer so native Codex execution can run inside the active OpenClaw sandbox. |
OpenClaw-owned dynamic tool calls are bounded independently from
`appServer.requestTimeoutMs`: Codex `item/tool/call` requests use a 90 second
@@ -574,10 +574,15 @@ goes quiet without `turn/completed`, OpenClaw best-effort interrupts the native
turn and releases the session lane. Post-tool raw assistant progress keeps
waiting for `turn/completed` while a completion-idle guard stays armed; the guard
uses `appServer.postToolRawAssistantCompletionIdleTimeoutMs` when configured and
falls back to the assistant completion idle timeout otherwise. Timeout
diagnostics include the last app-server notification method and, for raw
assistant response items, the item type, role, id, and a bounded assistant text
preview.
defaults to five minutes otherwise. Replay-safe stdio app-server failures,
including turn-completion idle timeouts without assistant, tool, active-item, or
side-effect evidence, are retried once on a fresh app-server attempt. Unsafe
timeouts still retire the stuck app-server client and release the OpenClaw
session lane. They also clear the stale native thread binding and surface a
recoverable timeout message for user or maintainer judgment instead of being
replayed automatically. Timeout diagnostics include the last app-server
notification method and, for raw assistant response items, the item type, role,
id, and a bounded assistant text preview.
Environment overrides remain available for local testing:

View File

@@ -1,13 +1,13 @@
---
summary: "Run OpenClaw embedded agent turns through the bundled GitHub Copilot SDK harness"
summary: "Run OpenClaw embedded agent turns through the external GitHub Copilot SDK harness"
title: "Copilot SDK harness"
read_when:
- You want to use the bundled GitHub Copilot SDK harness for an agent
- You want to use the GitHub Copilot SDK harness for an agent
- You need configuration examples for the `copilot` runtime
- You are wiring an agent to subscription Copilot (github / openclaw / copilot) and want it to run through the Copilot CLI
---
The bundled `copilot` extension lets OpenClaw run embedded subscription
The external `@openclaw/copilot` plugin lets OpenClaw run embedded subscription
Copilot agent turns through the GitHub Copilot CLI (`@github/copilot-sdk`)
instead of the built-in PI harness.
@@ -24,11 +24,11 @@ For the broader model/provider/runtime split, start with
## Requirements
- OpenClaw with the bundled `copilot` extension available.
- OpenClaw with the `@openclaw/copilot` plugin installed.
- If your config uses `plugins.allow`, include `copilot` (the manifest
id in `extensions/copilot/openclaw.plugin.json`). A restrictive
id declared by the plugin). A restrictive
allowlist that uses the npm-style `@openclaw/copilot` package name
will leave the bundled plugin blocked and the runtime will not load
will leave the plugin blocked and the runtime will not load
even with `agentRuntime.id: "copilot"`.
- A GitHub Copilot subscription that can drive the Copilot CLI (or a
`gitHubToken` env / auth-profile entry for headless / cron runs).
@@ -38,56 +38,38 @@ For the broader model/provider/runtime split, start with
or `~/.config/copilot` elsewhere) is used as the doctor probe fallback when
no explicit home is set.
`openclaw doctor` runs the bundled
`openclaw doctor` runs the plugin
[doctor contract](#doctor-and-probes) for the extension; failures there are
the canonical way to confirm the environment is ready before opting an agent
in.
## On-demand SDK install
## Plugin install
The Copilot agent runtime ships its small TypeScript code bundled inside
the openclaw tarball, but the underlying `@github/copilot-sdk` package
(and its platform-specific `@github/copilot-<platform>-<arch>` CLI
binary) is **not** installed by default — together they add ~260 MB to
your openclaw install footprint, and most openclaw users do not select
a Copilot model.
The Copilot runtime is an external plugin so the core `openclaw` package does
not carry the `@github/copilot-sdk` dependency or its platform-specific
`@github/copilot-<platform>-<arch>` CLI binary. Together they add roughly
260 MB, so install them only for agents that opt into this runtime:
The wizard offers to install the SDK the first time you select a
```bash
openclaw plugins install @openclaw/copilot
```
The wizard installs the plugin the first time you select a
`github-copilot/*` model **and** your config opts the model (or its
provider) into the Copilot agent runtime via
`agentRuntime: { id: "copilot" }` (see [Quickstart](#quickstart) below).
Without the opt-in, openclaw uses its built-in GitHub Copilot provider
and never prompts for the SDK install:
```
The Copilot agent runtime needs @github/copilot-sdk (~260 MB on first
install, downloads the @github/copilot CLI binary for your platform).
Install now? [Y/n]
```
If you accept, the SDK is installed into
`~/.openclaw/npm-runtime/copilot/` and detected on subsequent runs. The
install runs `npm ci` against a checked-in `package-lock.json` shipped
with openclaw at
`src/commands/copilot-sdk-install-manifest/package-lock.json`, so the
exact transitive graph reviewed for this release lands on disk on every
user machine.
If you decline, the runtime will fail at first invocation with an
actionable install message; re-run `openclaw setup` to retry the install
(or copy the pinned manifest into `~/.openclaw/npm-runtime/copilot/` and
run `npm ci` yourself if you need to install offline).
and never installs the runtime plugin.
The runtime resolves the SDK in this order:
1. `import("@github/copilot-sdk")` against the host openclaw install
(covers source/dev checkouts and any environment that pre-installs
the SDK alongside openclaw).
1. `import("@github/copilot-sdk")` from the installed `@openclaw/copilot`
package.
2. The well-known fallback dir `~/.openclaw/npm-runtime/copilot/` (the
wizard install target).
legacy on-demand install target).
A missing SDK surfaces a single error with code `COPILOT_SDK_MISSING`
and the manual install command above.
and the plugin reinstall command above.
## Quickstart

View File

@@ -169,6 +169,7 @@ or npm install metadata. Those belong in your plugin code and `package.json`.
| `modelIdNormalization` | No | `object` | Provider-owned model-id alias/prefix cleanup that must run before provider runtime loads. |
| `providerEndpoints` | No | `object[]` | Manifest-owned endpoint host/baseUrl metadata for provider routes that core must classify before provider runtime loads. |
| `providerRequest` | No | `object` | Cheap provider-family and request-compatibility metadata used by generic request policy before provider runtime loads. |
| `secretProviderIntegrations` | No | `Record<string, object>` | Declarative SecretRef exec provider presets that setup or install surfaces can offer without hardcoding provider-specific integrations in core. |
| `cliBackends` | No | `string[]` | CLI inference backend ids owned by this plugin. Used for startup auto-activation from explicit config refs. |
| `syntheticAuthRefs` | No | `string[]` | Provider or CLI backend refs whose plugin-owned synthetic auth hook should be probed during cold model discovery before runtime loads. |
| `nonSecretAuthMarkers` | No | `string[]` | Bundled-plugin-owned placeholder API key values that represent non-secret local, OAuth, or ambient credential state. |
@@ -1080,6 +1081,72 @@ Provider fields:
| `compatibilityFamily` | `"moonshot"` | Optional provider-family compatibility bucket for shared request helpers. |
| `openAICompletions` | `object` | OpenAI-compatible completions request flags, currently `supportsStreamingUsage`. |
## secretProviderIntegrations reference
Use `secretProviderIntegrations` when a plugin can publish a reusable SecretRef
exec provider preset. OpenClaw reads this metadata before plugin runtime loads,
stores plugin ownership in `secrets.providers.<alias>.pluginIntegration`, and
leaves actual secret resolution to the SecretRef runtime.
Presets are exposed only for bundled plugins and installed plugins discovered
from the managed plugin install roots, such as git and ClawHub installs.
```json
{
"secretProviderIntegrations": {
"secret-store": {
"providerAlias": "team-secrets",
"displayName": "Team secrets",
"source": "exec",
"command": "${node}",
"args": ["./bin/resolve-secrets.mjs"]
}
}
}
```
The map key is the integration id. If `providerAlias` is omitted, OpenClaw uses
the integration id as the SecretRef provider alias. Provider aliases must match
the normal SecretRef provider alias pattern, for example `team-secrets` or
`onepassword-work`.
When an operator selects the preset, OpenClaw writes a provider reference like:
```json
{
"secrets": {
"providers": {
"team-secrets": {
"source": "exec",
"pluginIntegration": {
"pluginId": "acme-secrets",
"integrationId": "secret-store"
}
}
}
}
}
```
At startup/reload, OpenClaw resolves that provider by loading current plugin
manifest metadata, checking that the owning plugin is installed and active, and
materializing the exec command from the manifest. Disabling or removing the
plugin revokes the provider for active SecretRefs. Operators who want standalone
exec configuration can still write manual `command`/`args` providers directly.
Only `source: "exec"` presets are currently supported. `command` must be
`${node}`, and `args[0]` must be a `./` plugin-root-relative resolver script.
OpenClaw materializes it at startup/reload to the current Node executable and
the absolute in-plugin script path. Node options such as `--require`, `--import`,
`--loader`, `--env-file`, `--eval`, and `--print` are not part of the manifest
preset contract. Operators who need non-Node commands can configure standalone
manual exec providers directly.
OpenClaw derives `trustedDirs` for manifest presets from the plugin root and,
for `${node}` presets, the current Node executable directory. Manifest-authored
`trustedDirs` are ignored. Other exec provider options such as `timeoutMs`,
`maxOutputBytes`, `jsonOnly`, `env`, `passEnv`, and `allowInsecurePath` pass
through to the normal SecretRef exec provider config.
## modelPricing reference
Use `modelPricing` when a provider needs control-plane pricing behavior before

View File

@@ -66,7 +66,6 @@ commands.
| [cloudflare-ai-gateway](/plugins/reference/cloudflare-ai-gateway) | Adds Cloudflare AI Gateway model provider support to OpenClaw. | `@openclaw/cloudflare-ai-gateway-provider`<br />included in OpenClaw | providers: cloudflare-ai-gateway |
| [codex-supervisor](/plugins/reference/codex-supervisor) | Supervise Codex app-server sessions from OpenClaw. | `@openclaw/codex-supervisor`<br />included in OpenClaw | contracts: tools |
| [comfy](/plugins/reference/comfy) | Adds ComfyUI model provider support to OpenClaw. | `@openclaw/comfy-provider`<br />included in OpenClaw | providers: comfy; contracts: imageGenerationProviders, musicGenerationProviders, videoGenerationProviders |
| [copilot](/plugins/reference/copilot) | Registers the GitHub Copilot agent runtime. | `@openclaw/copilot`<br />included in OpenClaw | plugin |
| [copilot-proxy](/plugins/reference/copilot-proxy) | Adds Copilot Proxy model provider support to OpenClaw. | `@openclaw/copilot-proxy`<br />included in OpenClaw | providers: copilot-proxy |
| [deepgram](/plugins/reference/deepgram) | Adds media understanding provider support. Adds realtime transcription provider support. | `@openclaw/deepgram-provider`<br />included in OpenClaw | contracts: mediaUnderstandingProviders, realtimeTranscriptionProviders |
| [deepinfra](/plugins/reference/deepinfra) | Adds DeepInfra model provider support to OpenClaw. | `@openclaw/deepinfra-provider`<br />included in OpenClaw | providers: deepinfra; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, speechProviders, videoGenerationProviders |
@@ -126,7 +125,6 @@ commands.
| [telegram](/plugins/reference/telegram) | Adds the Telegram channel surface for sending and receiving OpenClaw messages. | `@openclaw/telegram`<br />included in OpenClaw | channels: telegram |
| [tencent](/plugins/reference/tencent) | Adds Tencent TokenHub model provider support to OpenClaw. | `@openclaw/tencent-provider`<br />included in OpenClaw | providers: tencent-tokenhub |
| [together](/plugins/reference/together) | Adds Together model provider support to OpenClaw. | `@openclaw/together-provider`<br />included in OpenClaw | providers: together; contracts: videoGenerationProviders |
| [tokenjuice](/plugins/reference/tokenjuice) | Compacts exec and bash tool results with tokenjuice reducers. | `@openclaw/tokenjuice`<br />included in OpenClaw | contracts: agentToolResultMiddleware |
| [tts-local-cli](/plugins/reference/tts-local-cli) | Adds text-to-speech provider support. | `@openclaw/tts-local-cli`<br />included in OpenClaw | contracts: speechProviders |
| [venice](/plugins/reference/venice) | Adds Venice model provider support to OpenClaw. | `@openclaw/venice-provider`<br />included in OpenClaw | providers: venice |
| [vercel-ai-gateway](/plugins/reference/vercel-ai-gateway) | Adds Vercel AI Gateway model provider support to OpenClaw. | `@openclaw/vercel-ai-gateway-provider`<br />included in OpenClaw | providers: vercel-ai-gateway |
@@ -136,7 +134,7 @@ commands.
| [vydra](/plugins/reference/vydra) | Adds Vydra model provider support to OpenClaw. | `@openclaw/vydra-provider`<br />included in OpenClaw | providers: vydra; contracts: imageGenerationProviders, speechProviders, videoGenerationProviders |
| [web-readability](/plugins/reference/web-readability) | Extract readable article content from local HTML web fetch responses. | `@openclaw/web-readability-plugin`<br />included in OpenClaw | contracts: webContentExtractors |
| [webhooks](/plugins/reference/webhooks) | Authenticated inbound webhooks that bind external automation to OpenClaw TaskFlows. | `@openclaw/webhooks`<br />included in OpenClaw | plugin |
| [workboard](/plugins/reference/workboard) | Dashboard workboard for agent-owned issues and sessions. | `@openclaw/workboard`<br />included in OpenClaw | plugin |
| [workboard](/plugins/reference/workboard) | Dashboard workboard for agent-owned issues and sessions. | `@openclaw/workboard`<br />included in OpenClaw | contracts: tools |
| [xai](/plugins/reference/xai) | Adds xAI model provider support to OpenClaw. | `@openclaw/xai-plugin`<br />included in OpenClaw | providers: xai; contracts: imageGenerationProviders, mediaUnderstandingProviders, realtimeTranscriptionProviders, speechProviders, tools, videoGenerationProviders, webSearchProviders |
| [xiaomi](/plugins/reference/xiaomi) | Adds Xiaomi model provider support to OpenClaw. | `@openclaw/xiaomi-provider`<br />included in OpenClaw | providers: xiaomi; contracts: speechProviders |
| [zai](/plugins/reference/zai) | Adds Z.AI model provider support to OpenClaw. | `@openclaw/zai-provider`<br />included in OpenClaw | providers: zai; contracts: mediaUnderstandingProviders |
@@ -151,6 +149,7 @@ commands.
| [anthropic-vertex](/plugins/reference/anthropic-vertex) | OpenClaw Anthropic Vertex provider plugin for Claude models on Google Vertex AI. | `@openclaw/anthropic-vertex-provider`<br />npm; ClawHub | providers: anthropic-vertex |
| [brave](/plugins/reference/brave) | OpenClaw Brave Search provider plugin for web search. | `@openclaw/brave-plugin`<br />npm; ClawHub | contracts: webSearchProviders |
| [codex](/plugins/reference/codex) | OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog. | `@openclaw/codex`<br />npm; ClawHub | providers: codex; contracts: mediaUnderstandingProviders, migrationProviders |
| [copilot](/plugins/reference/copilot) | Registers the GitHub Copilot agent runtime. | `@openclaw/copilot`<br />npm; ClawHub: `clawhub:@openclaw/copilot` | plugin |
| [diagnostics-otel](/plugins/reference/diagnostics-otel) | OpenClaw diagnostics OpenTelemetry exporter for metrics and traces. | `@openclaw/diagnostics-otel`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-otel` | plugin |
| [diagnostics-prometheus](/plugins/reference/diagnostics-prometheus) | OpenClaw diagnostics Prometheus exporter for runtime metrics. | `@openclaw/diagnostics-prometheus`<br />npm; ClawHub: `clawhub:@openclaw/diagnostics-prometheus` | plugin |
| [diffs](/plugins/reference/diffs) | OpenClaw read-only diff viewer plugin and file renderer for agents. | `@openclaw/diffs`<br />npm; ClawHub | contracts: tools; skills |
@@ -167,11 +166,12 @@ commands.
| [nextcloud-talk](/plugins/reference/nextcloud-talk) | OpenClaw Nextcloud Talk channel plugin for conversations. | `@openclaw/nextcloud-talk`<br />npm; ClawHub | channels: nextcloud-talk |
| [nostr](/plugins/reference/nostr) | OpenClaw Nostr channel plugin for NIP-04 encrypted direct messages. | `@openclaw/nostr`<br />npm; ClawHub | channels: nostr |
| [openshell](/plugins/reference/openshell) | OpenClaw sandbox backend for the NVIDIA OpenShell CLI with mirrored local workspaces and SSH command execution. | `@openclaw/openshell-sandbox`<br />npm; ClawHub | plugin |
| [pixverse](/plugins/reference/pixverse) | OpenClaw PixVerse video generation provider plugin. | `@openclaw/pixverse-provider`<br />npm; ClawHub | contracts: videoGenerationProviders |
| [pixverse](/plugins/reference/pixverse) | OpenClaw PixVerse video generation provider plugin. | `@openclaw/pixverse-provider`<br />npm; ClawHub: `clawhub:@openclaw/pixverse-provider` | contracts: videoGenerationProviders |
| [qqbot](/plugins/reference/qqbot) | OpenClaw QQ Bot channel plugin for group and direct-message workflows. | `@openclaw/qqbot`<br />npm; ClawHub | channels: qqbot; contracts: tools; skills |
| [slack](/plugins/reference/slack) | OpenClaw Slack channel plugin for channels, DMs, commands, and app events. | `@openclaw/slack`<br />npm; ClawHub | channels: slack |
| [synology-chat](/plugins/reference/synology-chat) | Synology Chat channel plugin for OpenClaw channels and direct messages. | `@openclaw/synology-chat`<br />npm; ClawHub | channels: synology-chat |
| [tlon](/plugins/reference/tlon) | OpenClaw Tlon/Urbit channel plugin for chat workflows. | `@openclaw/tlon`<br />npm; ClawHub | channels: tlon; skills |
| [tokenjuice](/plugins/reference/tokenjuice) | Compacts exec and bash tool results with tokenjuice reducers. | `@openclaw/tokenjuice`<br />npm; ClawHub: `clawhub:@openclaw/tokenjuice` | contracts: agentToolResultMiddleware |
| [twitch](/plugins/reference/twitch) | OpenClaw Twitch channel plugin for chat and moderation workflows. | `@openclaw/twitch`<br />npm; ClawHub | channels: twitch |
| [voice-call](/plugins/reference/voice-call) | OpenClaw voice-call plugin for Twilio, Telnyx, and Plivo phone calls. | `@openclaw/voice-call`<br />npm; ClawHub | contracts: tools |
| [whatsapp](/plugins/reference/whatsapp) | OpenClaw WhatsApp channel plugin for WhatsApp Web chats. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |

View File

@@ -38,7 +38,7 @@ pnpm plugins:inventory:gen
| [codex](/plugins/reference/codex) | OpenClaw Codex app-server harness and model provider plugin with a Codex-managed GPT catalog. | `@openclaw/codex`<br />npm; ClawHub | providers: codex; contracts: mediaUnderstandingProviders, migrationProviders |
| [codex-supervisor](/plugins/reference/codex-supervisor) | Supervise Codex app-server sessions from OpenClaw. | `@openclaw/codex-supervisor`<br />included in OpenClaw | contracts: tools |
| [comfy](/plugins/reference/comfy) | Adds ComfyUI model provider support to OpenClaw. | `@openclaw/comfy-provider`<br />included in OpenClaw | providers: comfy; contracts: imageGenerationProviders, musicGenerationProviders, videoGenerationProviders |
| [copilot](/plugins/reference/copilot) | Registers the GitHub Copilot agent runtime. | `@openclaw/copilot`<br />included in OpenClaw | plugin |
| [copilot](/plugins/reference/copilot) | Registers the GitHub Copilot agent runtime. | `@openclaw/copilot`<br />npm; ClawHub: `clawhub:@openclaw/copilot` | plugin |
| [copilot-proxy](/plugins/reference/copilot-proxy) | Adds Copilot Proxy model provider support to OpenClaw. | `@openclaw/copilot-proxy`<br />included in OpenClaw | providers: copilot-proxy |
| [deepgram](/plugins/reference/deepgram) | Adds media understanding provider support. Adds realtime transcription provider support. | `@openclaw/deepgram-provider`<br />included in OpenClaw | contracts: mediaUnderstandingProviders, realtimeTranscriptionProviders |
| [deepinfra](/plugins/reference/deepinfra) | Adds DeepInfra model provider support to OpenClaw. | `@openclaw/deepinfra-provider`<br />included in OpenClaw | providers: deepinfra; contracts: imageGenerationProviders, mediaUnderstandingProviders, memoryEmbeddingProviders, speechProviders, videoGenerationProviders |
@@ -99,7 +99,7 @@ pnpm plugins:inventory:gen
| [openrouter](/plugins/reference/openrouter) | Adds OpenRouter model provider support to OpenClaw. | `@openclaw/openrouter-provider`<br />included in OpenClaw | providers: openrouter; contracts: imageGenerationProviders, mediaUnderstandingProviders, musicGenerationProviders, speechProviders, videoGenerationProviders |
| [openshell](/plugins/reference/openshell) | OpenClaw sandbox backend for the NVIDIA OpenShell CLI with mirrored local workspaces and SSH command execution. | `@openclaw/openshell-sandbox`<br />npm; ClawHub | plugin |
| [perplexity](/plugins/reference/perplexity) | Adds web search provider support. | `@openclaw/perplexity-plugin`<br />included in OpenClaw | contracts: webSearchProviders |
| [pixverse](/plugins/reference/pixverse) | OpenClaw PixVerse video generation provider plugin. | `@openclaw/pixverse-provider`<br />npm; ClawHub | contracts: videoGenerationProviders |
| [pixverse](/plugins/reference/pixverse) | OpenClaw PixVerse video generation provider plugin. | `@openclaw/pixverse-provider`<br />npm; ClawHub: `clawhub:@openclaw/pixverse-provider` | contracts: videoGenerationProviders |
| [policy](/plugins/reference/policy) | Adds policy-backed doctor checks for workspace conformance. | `@openclaw/policy`<br />included in OpenClaw | plugin |
| [qa-channel](/plugins/reference/qa-channel) | Adds the QA Channel surface for sending and receiving OpenClaw messages. | `@openclaw/qa-channel`<br />source checkout only | channels: qa-channel |
| [qa-lab](/plugins/reference/qa-lab) | OpenClaw QA lab plugin with private debugger UI and scenario runner. | `@openclaw/qa-lab`<br />source checkout only | plugin |
@@ -122,7 +122,7 @@ pnpm plugins:inventory:gen
| [tencent](/plugins/reference/tencent) | Adds Tencent TokenHub model provider support to OpenClaw. | `@openclaw/tencent-provider`<br />included in OpenClaw | providers: tencent-tokenhub |
| [tlon](/plugins/reference/tlon) | OpenClaw Tlon/Urbit channel plugin for chat workflows. | `@openclaw/tlon`<br />npm; ClawHub | channels: tlon; skills |
| [together](/plugins/reference/together) | Adds Together model provider support to OpenClaw. | `@openclaw/together-provider`<br />included in OpenClaw | providers: together; contracts: videoGenerationProviders |
| [tokenjuice](/plugins/reference/tokenjuice) | Compacts exec and bash tool results with tokenjuice reducers. | `@openclaw/tokenjuice`<br />included in OpenClaw | contracts: agentToolResultMiddleware |
| [tokenjuice](/plugins/reference/tokenjuice) | Compacts exec and bash tool results with tokenjuice reducers. | `@openclaw/tokenjuice`<br />npm; ClawHub: `clawhub:@openclaw/tokenjuice` | contracts: agentToolResultMiddleware |
| [tts-local-cli](/plugins/reference/tts-local-cli) | Adds text-to-speech provider support. | `@openclaw/tts-local-cli`<br />included in OpenClaw | contracts: speechProviders |
| [twitch](/plugins/reference/twitch) | OpenClaw Twitch channel plugin for chat and moderation workflows. | `@openclaw/twitch`<br />npm; ClawHub | channels: twitch |
| [venice](/plugins/reference/venice) | Adds Venice model provider support to OpenClaw. | `@openclaw/venice-provider`<br />included in OpenClaw | providers: venice |
@@ -135,7 +135,7 @@ pnpm plugins:inventory:gen
| [web-readability](/plugins/reference/web-readability) | Extract readable article content from local HTML web fetch responses. | `@openclaw/web-readability-plugin`<br />included in OpenClaw | contracts: webContentExtractors |
| [webhooks](/plugins/reference/webhooks) | Authenticated inbound webhooks that bind external automation to OpenClaw TaskFlows. | `@openclaw/webhooks`<br />included in OpenClaw | plugin |
| [whatsapp](/plugins/reference/whatsapp) | OpenClaw WhatsApp channel plugin for WhatsApp Web chats. | `@openclaw/whatsapp`<br />ClawHub: `clawhub:@openclaw/whatsapp`; npm | channels: whatsapp |
| [workboard](/plugins/reference/workboard) | Dashboard workboard for agent-owned issues and sessions. | `@openclaw/workboard`<br />included in OpenClaw | plugin |
| [workboard](/plugins/reference/workboard) | Dashboard workboard for agent-owned issues and sessions. | `@openclaw/workboard`<br />included in OpenClaw | contracts: tools |
| [xai](/plugins/reference/xai) | Adds xAI model provider support to OpenClaw. | `@openclaw/xai-plugin`<br />included in OpenClaw | providers: xai; contracts: imageGenerationProviders, mediaUnderstandingProviders, realtimeTranscriptionProviders, speechProviders, tools, videoGenerationProviders, webSearchProviders |
| [xiaomi](/plugins/reference/xiaomi) | Adds Xiaomi model provider support to OpenClaw. | `@openclaw/xiaomi-provider`<br />included in OpenClaw | providers: xiaomi; contracts: speechProviders |
| [zai](/plugins/reference/zai) | Adds Z.AI model provider support to OpenClaw. | `@openclaw/zai-provider`<br />included in OpenClaw | providers: zai; contracts: mediaUnderstandingProviders |

View File

@@ -17,3 +17,7 @@ Supervise Codex app-server sessions from OpenClaw.
## Surface
contracts: tools
## Session Listing
`codex_sessions_list` defaults to loaded Codex sessions only. Set `include_stored` to include stored history; the plugin uses Codex app-server's state-DB-only listing path and caps stored results at 200 by default. Pass `max_stored_sessions` to lower or raise that cap, up to 1000.

View File

@@ -12,7 +12,7 @@ Registers the GitHub Copilot agent runtime.
## Distribution
- Package: `@openclaw/copilot`
- Install route: included in OpenClaw
- Install route: npm; ClawHub: `clawhub:@openclaw/copilot`
## Surface

View File

@@ -12,7 +12,7 @@ OpenClaw PixVerse video generation provider plugin.
## Distribution
- Package: `@openclaw/pixverse-provider`
- Install route: npm; ClawHub
- Install route: npm; ClawHub: `clawhub:@openclaw/pixverse-provider`
## Surface

View File

@@ -12,7 +12,7 @@ Compacts exec and bash tool results with tokenjuice reducers.
## Distribution
- Package: `@openclaw/tokenjuice`
- Install route: included in OpenClaw
- Install route: npm; ClawHub: `clawhub:@openclaw/tokenjuice`
## Surface

View File

@@ -16,7 +16,7 @@ Dashboard workboard for agent-owned issues and sessions.
## Surface
plugin
contracts: tools
## Related docs

View File

@@ -155,6 +155,7 @@ Most channel plugins do not need approval-specific code.
- If custom approval auth intentionally allows only same-chat fallback, return `markImplicitSameChatApprovalAuthorization({ authorized: true })` from `openclaw/plugin-sdk/approval-auth-runtime`; otherwise core treats the result as explicit approver authorization.
- If a channel-owned native callback resolves approvals directly, use `isImplicitSameChatApprovalAuthorization(...)` before resolving so implicit fallback still goes through the channel's normal actor authorization.
- If a channel needs native approval delivery, keep channel code focused on target normalization plus transport/presentation facts. Use `createChannelExecApprovalProfile`, `createChannelNativeOriginTargetResolver`, `createChannelApproverDmTargetResolver`, and `createApproverRestrictedNativeApprovalCapability` from `openclaw/plugin-sdk/approval-runtime`. Put the channel-specific facts behind `approvalCapability.nativeRuntime`, ideally via `createChannelApprovalNativeRuntimeAdapter(...)` or `createLazyChannelApprovalNativeRuntimeAdapter(...)`, so core can assemble the handler and own request filtering, routing, dedupe, expiry, gateway subscription, and routed-elsewhere notices. `nativeRuntime` is split into a few smaller seams:
- Use `createNativeApprovalChannelRouteGates` from `openclaw/plugin-sdk/approval-native-runtime` when a channel supports both session-origin native delivery and explicit approval forwarding targets. The helper centralizes approval config selection, `mode` handling, agent/session filters, account binding, session-target matching, and target-list matching while callers still own the channel id, default forwarding mode, account lookup, transport-enabled check, target normalization, and turn-source target resolution. Do not use it to create core-owned channel policy defaults; pass the channel's documented default mode explicitly.
- `createChannelNativeOriginTargetResolver` uses the shared channel-route matcher by default for `{ to, accountId, threadId }` targets. Pass `targetsMatch` only when a channel has provider-specific equivalence rules, such as Slack timestamp prefix matching.
- Pass `normalizeTargetForMatch` to `createChannelNativeOriginTargetResolver` when the channel needs to canonicalize provider ids before the default route matcher or a custom `targetsMatch` callback runs, while preserving the original target for delivery. Use `normalizeTarget` only when the resolved delivery target itself should be canonicalized.
- `availability` - whether the account is configured and whether a request should be handled

View File

@@ -144,6 +144,7 @@ and pairing-path families.
| `plugin-sdk/self-hosted-provider-setup` | Focused OpenAI-compatible self-hosted provider setup helpers |
| `plugin-sdk/cli-backend` | CLI backend defaults + watchdog constants |
| `plugin-sdk/provider-auth-runtime` | Runtime API-key resolution helpers for provider plugins |
| `plugin-sdk/provider-oauth-runtime` | Generic provider OAuth callback types, callback-page rendering, PKCE/state helpers, and abort helpers |
| `plugin-sdk/provider-auth-api-key` | API-key onboarding/profile-write helpers such as `upsertApiKeyProfile` |
| `plugin-sdk/provider-auth-result` | Standard OAuth auth-result builder |
| `plugin-sdk/provider-env-vars` | Provider auth env-var lookup helpers |
@@ -179,7 +180,7 @@ and pairing-path families.
| `plugin-sdk/approval-gateway-runtime` | Shared approval gateway-resolution helper |
| `plugin-sdk/approval-handler-adapter-runtime` | Lightweight native approval adapter loading helpers for hot channel entrypoints |
| `plugin-sdk/approval-handler-runtime` | Broader approval handler runtime helpers; prefer the narrower adapter/gateway seams when they are enough |
| `plugin-sdk/approval-native-runtime` | Native approval target + account-binding helpers and local native exec prompt suppression |
| `plugin-sdk/approval-native-runtime` | Native approval target, account-binding, route-gate, forwarding fallback, and local native exec prompt suppression helpers |
| `plugin-sdk/approval-reaction-runtime` | Hardcoded approval reaction bindings, reaction prompt payloads, reaction target stores, and compatibility export for local native exec prompt suppression |
| `plugin-sdk/approval-reply-runtime` | Exec/plugin approval reply payload helpers |
| `plugin-sdk/approval-runtime` | Exec/plugin approval payload helpers, native approval routing/runtime helpers, and structured approval display helpers such as `formatApprovalDisplayPath` |
@@ -192,6 +193,7 @@ and pairing-path families.
| `plugin-sdk/allow-from` | `formatAllowFromLowercase` |
| `plugin-sdk/channel-secret-runtime` | Narrow secret-contract collection helpers for channel/plugin secret surfaces |
| `plugin-sdk/secret-ref-runtime` | Narrow `coerceSecretRef` and SecretRef typing helpers for secret-contract/config parsing |
| `plugin-sdk/secret-provider-integration` | Type-only SecretRef provider integration manifest and preset contracts for plugins that publish external secret provider presets |
| `plugin-sdk/security-runtime` | Shared trust, DM gating, root-bounded file/path helpers including create-only writes, sync/async atomic file replacement, sibling temp writes, cross-device move fallback, private file-store helpers, symlink-parent guards, external-content, sensitive text redaction, constant-time secret comparison, and secret-collection helpers |
| `plugin-sdk/ssrf-policy` | Host allowlist and private-network SSRF policy helpers |
| `plugin-sdk/ssrf-dispatcher` | Narrow pinned-dispatcher helpers without the broad infra runtime surface |
@@ -219,6 +221,7 @@ and pairing-path families.
| `plugin-sdk/lazy-runtime` | Lazy runtime import/binding helpers such as `createLazyRuntimeModule`, `createLazyRuntimeMethod`, and `createLazyRuntimeSurface` |
| `plugin-sdk/process-runtime` | Process exec helpers |
| `plugin-sdk/cli-runtime` | CLI formatting, wait, version, argument-invocation, and lazy command-group helpers |
| `plugin-sdk/qa-live-transport-scenarios` | Shared live transport QA scenario ids, baseline coverage helpers, and scenario-selection helper |
| `plugin-sdk/gateway-method-runtime` | Reserved Gateway method dispatch helper for plugin HTTP routes that declare `contracts.gatewayMethodDispatch: ["authenticated-request"]` |
| `plugin-sdk/gateway-runtime` | Gateway client, event-loop-ready client start helper, gateway CLI RPC, gateway protocol errors, and channel-status patch helpers |
| `plugin-sdk/config-contracts` | Focused type-only config surface for plugin config shapes such as `OpenClawConfig` and channel/provider config types |

View File

@@ -48,8 +48,8 @@ Each card stores:
- optional agent id
- optional linked session, run, task, or source URL
- optional execution metadata for a Codex or Claude session started from the card
- compact metadata for attempts, comments, links, proof, templates, archive state, and stale-session detection
- recent card events such as created, moved, linked, attempt, proof, archive, stale, or agent-updated changes
- compact metadata for attempts, comments, links, proof, artifacts, claims, diagnostics, notifications, templates, archive state, and stale-session detection
- recent card events such as created, moved, linked, claimed, heartbeat, attempt, proof, artifact, diagnostic, notification, archive, stale, or agent-updated changes
Cards are stored in the plugin's Gateway state. They are local to the Gateway
state directory and move with the rest of that Gateway's OpenClaw state.
@@ -80,6 +80,31 @@ Each linked execution also records an attempt summary on the same card record.
The attempt summary keeps the engine, mode, model, run id, timestamps, status,
and rolling failure count so repeated failures remain visible on the board.
## Agent coordination
Workboard also exposes optional agent tools for board-aware workflows:
- `workboard_list` lists compact cards with claim and diagnostic state.
- `workboard_read` returns one card plus bounded worker context built from notes,
attempts, comments, links, proof, artifacts, and active diagnostics.
- `workboard_claim` claims a card for the calling agent and moves backlog or todo
cards into `running`.
- `workboard_heartbeat` refreshes the claim heartbeat during longer runs.
- `workboard_release` releases the claim after completion, pause, or handoff and
can move the card to a next status.
- `workboard_comment`, `workboard_proof`, and `workboard_unblock` let an agent
add handoff notes, attach proof or artifact references, and move blocked work
back to `todo`.
Claimed cards reject agent-tool mutations from other agents unless the caller
has the claim token returned by `workboard_claim`. Dashboard operators still use
the normal Gateway RPC surface and can recover or reassign cards.
Workboard diagnostics are computed from local card metadata. The built-in checks
flag assigned cards that wait too long, running cards without recent heartbeat,
blocked cards that need attention, repeated failures, done cards without proof,
and running cards that only have a loose session link.
## Session lifecycle sync
Cards can be linked to existing dashboard sessions or to the session created
@@ -136,7 +161,10 @@ The plugin registers Gateway RPC methods under the `workboard.*` namespace:
- `workboard.cards.list` requires `operator.read`
- `workboard.cards.export` requires `operator.read`
- create, update, move, delete, comment, link, proof, and archive methods require `operator.write`
- `workboard.cards.diagnostics` requires `operator.read`
- `workboard.cards.diagnostics.refresh` requires `operator.write`
- create, update, move, delete, comment, link, proof, artifact, claim, heartbeat,
release, unblock, bulk, and archive methods require `operator.write`
Browsers connected with read-only operator access can inspect the board but
cannot mutate cards.

View File

@@ -3,14 +3,15 @@ summary: "Sign in to GitHub Copilot from OpenClaw using the device flow or non-i
read_when:
- You want to use GitHub Copilot as a model provider
- You need the `openclaw models auth login-github-copilot` flow
- You are choosing between the built-in Copilot provider, Copilot SDK harness, and Copilot Proxy
title: "GitHub Copilot"
---
GitHub Copilot is GitHub's AI coding assistant. It provides access to Copilot
models for your GitHub account and plan. OpenClaw can use Copilot as a model
provider in two different ways.
provider or agent runtime in three different ways.
## Two ways to use Copilot in OpenClaw
## Three ways to use Copilot in OpenClaw
<Tabs>
<Tab title="Built-in provider (github-copilot)">
@@ -46,6 +47,38 @@ provider in two different ways.
</Tab>
<Tab title="Copilot SDK harness plugin (copilot)">
Install the external `@openclaw/copilot` plugin when you want GitHub's
Copilot CLI and SDK to own the low-level agent loop for selected
`github-copilot/*` models.
```bash
openclaw plugins install clawhub:@openclaw/copilot
```
Then opt a model or provider into the runtime:
```json5
{
agents: {
defaults: {
model: "github-copilot/gpt-5.5",
models: {
"github-copilot/gpt-5.5": {
agentRuntime: { id: "copilot" },
},
},
},
},
}
```
Choose this when you want native Copilot CLI sessions, SDK-managed thread
state, and Copilot-owned compaction for those agent turns. See
[Copilot SDK harness](/plugins/copilot) for the full runtime contract.
</Tab>
<Tab title="Copilot Proxy plugin (copilot-proxy)">
Use the **Copilot Proxy** VS Code extension as a local bridge. OpenClaw talks to
the proxy's `/v1` endpoint and uses the model list you configure there.

View File

@@ -679,7 +679,7 @@ Use these as starting points and replace model IDs with the exact names from `ol
```
Use `compat.supportsTools: false` only when the model or server reliably fails on tool schemas. It trades agent capability for stability.
`localModelLean` removes the browser, cron, and message tools from the model-visible agent surface, but it does not change Ollama's runtime context or thinking mode. If Code Mode or Tool Search is enabled, those tools can still be called from the hidden catalog. Pair lean mode with explicit `params.num_ctx` and `params.thinking: false` for small Qwen-style thinking models that loop or spend their response budget on hidden reasoning.
`localModelLean` removes the browser, cron, and message tools from the agent surface, but it does not change Ollama's runtime context or thinking mode. Pair it with explicit `params.num_ctx` and `params.thinking: false` for small Qwen-style thinking models that loop or spend their response budget on hidden reasoning.
</Accordion>
</AccordionGroup>

View File

@@ -25,7 +25,7 @@ OpenClaw provides `pixverse` as an official external plugin for hosted PixVerse
<Steps>
<Step title="Install the plugin">
```bash
openclaw plugins install @openclaw/pixverse-provider
openclaw plugins install clawhub:@openclaw/pixverse-provider
openclaw gateway restart
```
</Step>

File diff suppressed because it is too large Load Diff

View File

@@ -482,6 +482,42 @@ Notes:
- See the [Browserbase docs](https://docs.browserbase.com) for full API
reference, SDK guides, and integration examples.
### Notte
[Notte](https://www.notte.cc) is a cloud platform for running headless
browsers with built-in stealth, residential proxies, and a CDP-native
WebSocket gateway.
```json5
{
browser: {
enabled: true,
defaultProfile: "notte",
remoteCdpTimeoutMs: 3000,
remoteCdpHandshakeTimeoutMs: 5000,
profiles: {
notte: {
cdpUrl: "wss://us-prod.notte.cc/sessions/connect?token=<NOTTE_API_KEY>",
color: "#7C3AED",
},
},
},
}
```
Notes:
- [Sign up](https://console.notte.cc) and copy your **API Key** from the
console settings page.
- Replace `<NOTTE_API_KEY>` with your real Notte API key.
- Notte auto-creates a browser session on WebSocket connect, so no manual
session creation step is needed. The session is destroyed when the
WebSocket disconnects.
- The free tier allows five concurrent sessions and 100 lifetime browser
hours. See [pricing](https://www.notte.cc/#pricing) for paid plan limits.
- See the [Notte docs](https://docs.notte.cc) for full API reference, SDK
guides, and integration examples.
## Security
Key ideas:

View File

@@ -213,7 +213,7 @@ Common aliases such as `js`, `ts`, `bash`, `md`, `yml`, `c++`, `dockerfile`, `rb
Install the Diff Viewer Language Pack plugin to highlight other languages:
```bash
openclaw plugins install diffs-language-pack
openclaw plugins install clawhub:@openclaw/diffs-language-pack
```
With the language pack available, OpenClaw automatically uses it for languages outside the default list. Without it, those files stay readable as plain text.

View File

@@ -109,7 +109,7 @@ Notes:
- YOLO comes from the host-policy defaults (`security=full`, `ask=off`), not from `host=auto`. If you want to force gateway or node routing, set `tools.exec.host` or use `/exec host=...`.
- In `security=full` plus `ask=off` mode, host exec follows the configured policy directly; there is no extra heuristic command-obfuscation prefilter or script-preflight rejection layer.
- `tools.exec.node` (default: unset)
- `tools.exec.strictInlineEval` (default: false): when true, inline interpreter eval forms such as `python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`, `lua -e`, and `osascript -e` require reviewer or explicit approval. In `mode=auto`, the native auto reviewer may allow a clearly low-risk one-off command; if the reviewer asks, the request goes to a human. `allow-always` can still persist benign interpreter/script invocations, but inline-eval forms do not become durable allow rules.
- `tools.exec.strictInlineEval` (default: false): when true, inline interpreter eval forms such as `python -c`, `node -e`, `ruby -e`, `perl -e`, `php -r`, `lua -e`, and `osascript -e` require reviewer or explicit approval. In `mode=auto`, the normal exec approval path may let the native auto reviewer allow a clearly low-risk one-off command; direct node-host `system.run` calls still require an explicit approval because they cannot hand the command to a human approval route. If the reviewer asks, the request goes to a human. `allow-always` can still persist benign interpreter/script invocations, but inline-eval forms do not become durable allow rules.
- `tools.exec.commandHighlighting` (default: false): when true, approval prompts can highlight parser-derived command spans in the command text. Set to `true` globally or per agent to enable command text highlighting without changing exec approval policy.
- `tools.exec.pathPrepend`: list of directories to prepend to `PATH` for exec runs (gateway + sandbox only).
- `tools.exec.safeBins`: stdin-only safe binaries that can run without explicit allowlist entries. For behavior details, see [Safe bins](/tools/exec-approvals-advanced#safe-bins-stdin-only).

217
docs/tools/goal.md Normal file
View File

@@ -0,0 +1,217 @@
---
doc-schema-version: 1
summary: "Session goals: durable per-session objectives, /goal controls, model goal tools, token budgets, and TUI status"
read_when:
- You want OpenClaw to keep one objective visible across a long session
- You need to pause, resume, block, complete, or clear a session goal
- You want to understand the get_goal, create_goal, and update_goal tools
- You want to see how goals appear in the TUI
title: "Goal"
---
# Goal
A **goal** is one durable objective attached to the current OpenClaw session.
It gives the agent and the operator a shared target for long-running work,
without turning that target into a background task, reminder, cron job, or
standing order.
Goals are session state. They move with the session key, survive process
restarts, show up in `/goal`, are available to the model through the goal
tools, and appear in the TUI footer when the active session has one.
## Quick start
Set a goal:
```text
/goal start get CI green for PR 87469 and push the fix
```
Check it:
```text
/goal
```
Pause it when work is intentionally waiting:
```text
/goal pause waiting for CI
```
Resume it:
```text
/goal resume
```
Mark it complete:
```text
/goal complete pushed and verified
```
Clear it:
```text
/goal clear
```
## What goals are for
Use a goal when a session has a concrete outcome that should remain visible
across many turns:
- A PR closeout: fix, verify, autoreview, push, and open or update the PR.
- A debug run: reproduce the bug, identify the owning surface, patch, and prove
the fix.
- A docs pass: read the relevant docs, write the new page, cross-link it, and
verify the docs build.
- A maintenance task: inspect current state, make bounded changes, run the right
checks, and report what changed.
A goal is not a task queue. Use [Task Flow](/automation/taskflow),
[tasks](/automation/tasks), [cron jobs](/automation/cron-jobs), or
[standing orders](/automation/standing-orders) when work should run detached,
repeat on a schedule, fan out into managed sub-work, or persist as a policy.
## Command reference
`/goal` without arguments prints the current goal summary:
```text
Goal
Status: active
Objective: get CI green for PR 87469 and push the fix
Tokens used: 12k
Token budget: 12k/50k
Commands: /goal pause, /goal complete, /goal clear
```
Commands:
- `/goal` or `/goal status` shows the current goal.
- `/goal start <objective>` creates a new goal for the current session.
- `/goal set <objective>` and `/goal create <objective>` are aliases for
`start`.
- `/goal pause [note]` pauses an active goal.
- `/goal resume [note]` resumes a paused, blocked, usage-limited, or
budget-limited goal.
- `/goal complete [note]` marks the goal achieved.
- `/goal done [note]` is an alias for `complete`.
- `/goal block [note]` marks the goal blocked.
- `/goal blocked [note]` is an alias for `block`.
- `/goal clear` removes the goal from the session.
Only one goal can exist on a session at a time. Starting a second goal fails
until the current one is cleared.
## Statuses
Goals use a small status set:
- `active`: the session is pursuing the goal.
- `paused`: the operator paused the goal; `/goal resume` makes it active again.
- `blocked`: the agent or operator reported a real blocker; `/goal resume`
makes it active again when new information or state is available.
- `budget_limited`: the configured token budget was reached; `/goal resume`
restarts pursuit from the same objective.
- `usage_limited`: reserved for usage-limit stop states; `/goal resume`
restarts pursuit when allowed.
- `complete`: the goal was achieved. Complete goals are terminal; use
`/goal clear` before starting another goal.
`/new` and `/reset` clear the current session goal because they intentionally
start fresh session context.
## Token budgets
Goals can have an optional positive token budget. The budget is stored with the
goal and measured from the session's fresh token count at creation time. If the
current session only has stale or unknown token usage when the goal starts,
OpenClaw waits for the next fresh session token snapshot and uses that as the
baseline, so tokens spent before the goal existed are not charged to the goal.
When token usage reaches the budget, the goal changes to `budget_limited`. This
does not delete the goal or erase the objective. It tells the operator and the
agent that the goal is no longer actively being pursued until it is resumed or
cleared.
Token budgets are a session-goal guardrail, not a billing cap. Provider quota,
cost reporting, and context-window behavior still use the normal OpenClaw
usage and model controls.
## Model tools
OpenClaw exposes three core goal tools to agent harnesses:
- `get_goal`: read the current session goal, including status, objective, token
usage, and token budget.
- `create_goal`: create a goal only when the user, system, or developer
instructions explicitly request one. It fails if the session already has a
goal.
- `update_goal`: mark the goal `complete` or `blocked`.
The model cannot silently pause, resume, clear, or replace a goal. Those are
operator/session controls through `/goal` and reset commands. This keeps the
agent from quietly moving the target while preserving a clean path for the
agent to report achievement or a genuine blocker.
The `update_goal` tool should mark a goal `complete` only when the objective is
actually achieved. It should mark a goal `blocked` only when the same blocking
condition has repeated and the agent cannot make meaningful progress without
new user input or an external-state change.
## TUI
The TUI keeps the active session's goal visible in the footer next to the
agent, session, model, run controls, and token counts.
Footer examples:
- `Pursuing goal (12k/50k)` for an active goal with a token budget.
- `Goal paused (/goal resume)` for a paused goal.
- `Goal blocked (/goal resume)` for a blocked goal.
- `Goal hit usage limits (/goal resume)` for a usage-limited goal.
- `Goal unmet (50k/50k)` for a budget-limited goal.
- `Goal achieved (42k)` for a completed goal.
The footer is intentionally compact. Use `/goal` for the full objective, note,
token budget, and available commands.
## Channel behavior
The `/goal` command works in command-capable OpenClaw sessions, including the
TUI and chat surfaces that permit text commands. Goal state is attached to the
session key, not the transport. If two surfaces use the same session, they see
the same goal.
Goal state is not a delivery directive. It does not force replies through a
channel, change queue behavior, approve tools, or schedule work.
## Troubleshooting
`Goal error: goal already exists` means the session already has a goal. Use
`/goal` to inspect it, `/goal complete` if it is done, or `/goal clear` before
starting a different objective.
`Goal error: goal not found` means the session has no goal yet. Start one with
`/goal start <objective>`.
`Goal error: goal is already complete` means the goal is terminal. Clear it
before starting or resuming another objective.
If token usage looks like `0` or stale, the active session may not have a fresh
token snapshot yet. Usage refreshes as OpenClaw records session usage and
transcript-derived totals.
## Related
- [Slash commands](/tools/slash-commands)
- [TUI](/web/tui)
- [Session tool](/concepts/session-tool)
- [Compaction](/concepts/compaction)
- [Task Flow](/automation/taskflow)
- [Standing orders](/automation/standing-orders)

View File

@@ -11,11 +11,12 @@ sidebarTitle: "Image generation"
The `image_generate` tool lets the agent create and edit images using your
configured providers. In chat sessions, image generation runs asynchronously:
OpenClaw records a background task, returns the task id immediately, and wakes
the agent when the provider finishes. The completion agent must send generated
images through the `message` tool. If the requester session is inactive or
its active wake fails, and some generated images are still missing from
message-tool delivery, OpenClaw sends an idempotent direct fallback with only
the missing images.
the agent when the provider finishes. The completion agent follows the
session's normal visible-reply mode: automatic final reply delivery when
configured, or `message(action="send")` when the session requires the message
tool. If the requester session is inactive or its active wake fails, and some
generated images are still missing from the completion reply, OpenClaw sends an
idempotent direct fallback with only the missing images.
<Note>
The tool only appears when at least one image-generation provider is

View File

@@ -78,18 +78,18 @@ The table lists representative tools so you can recognize the surface. It is
not the full policy reference. For exact groups, defaults, and allow/deny
semantics, use [Tools and custom providers](/gateway/config-tools).
| Category | Use when the agent needs to... | Representative tools | Read next |
| ----------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| Runtime | Run commands, manage processes, or use provider-backed Python analysis | `exec`, `process`, `code_execution` | [Exec](/tools/exec), [Code execution](/tools/code-execution) |
| Files | Read and change workspace files | `read`, `write`, `edit`, `apply_patch` | [Apply patch](/tools/apply-patch) |
| Web | Search the web, search X posts, or fetch readable page content | `web_search`, `x_search`, `web_fetch` | [Web tools](/tools/web), [Web fetch](/tools/web-fetch) |
| Browser | Operate a browser session | `browser` | [Browser](/tools/browser) |
| Messaging and channels | Send replies or channel actions | `message` | [Agent send](/tools/agent-send) |
| Sessions and agents | Inspect sessions, delegate work, steer another run, or report status | `sessions_*`, `subagents`, `agents_list`, `session_status` | [Sub-agents](/tools/subagents), [Session tool](/concepts/session-tool) |
| Automation | Schedule work or respond to background events | `cron`, `heartbeat_respond` | [Automation](/automation) |
| Gateway and nodes | Inspect Gateway state or paired target devices | `gateway`, `nodes` | [Gateway configuration](/gateway/configuration), [Nodes](/nodes) |
| Media | Analyze, generate, or speak media | `image`, `image_generate`, `music_generate`, `video_generate`, `tts` | [Media overview](/tools/media-overview) |
| Large OpenClaw catalogs | Search and call many eligible tools without sending every schema to the model | `tool_search_code`, `tool_search`, `tool_describe` | [Tool Search](/tools/tool-search) |
| Category | Use when the agent needs to... | Representative tools | Read next |
| ----------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Runtime | Run commands, manage processes, or use provider-backed Python analysis | `exec`, `process`, `code_execution` | [Exec](/tools/exec), [Code execution](/tools/code-execution) |
| Files | Read and change workspace files | `read`, `write`, `edit`, `apply_patch` | [Apply patch](/tools/apply-patch) |
| Web | Search the web, search X posts, or fetch readable page content | `web_search`, `x_search`, `web_fetch` | [Web tools](/tools/web), [Web fetch](/tools/web-fetch) |
| Browser | Operate a browser session | `browser` | [Browser](/tools/browser) |
| Messaging and channels | Send replies or channel actions | `message` | [Agent send](/tools/agent-send) |
| Sessions and agents | Inspect sessions, delegate work, steer another run, or report status | `sessions_*`, `subagents`, `agents_list`, `session_status`, `goal` | [Goal](/tools/goal), [Sub-agents](/tools/subagents), [Session tool](/concepts/session-tool) |
| Automation | Schedule work or respond to background events | `cron`, `heartbeat_respond` | [Automation](/automation) |
| Gateway and nodes | Inspect Gateway state or paired target devices | `gateway`, `nodes` | [Gateway configuration](/gateway/configuration), [Nodes](/nodes) |
| Media | Analyze, generate, or speak media | `image`, `image_generate`, `music_generate`, `video_generate`, `tts` | [Media overview](/tools/media-overview) |
| Large OpenClaw catalogs | Search and call many eligible tools without sending every schema to the model | `tool_search_code`, `tool_search`, `tool_describe` | [Tool Search](/tools/tool-search) |
<Note>
Tool Search is an experimental OpenClaw agent surface. Codex harness runs use

View File

@@ -98,11 +98,12 @@ For async tools, OpenClaw submits the request to the provider, returns a task
id immediately, and tracks the job in the task ledger. The agent continues
responding to other messages while the job runs. When the provider finishes,
OpenClaw wakes the agent with the generated media paths so it can tell the
user and relay the result through the message tool. If the requester session
is inactive or its active wake fails, and some generated media is still
missing from message-tool delivery, OpenClaw sends an idempotent direct
fallback with only the missing media. Media already delivered through the
message tool is not posted again.
user through the session's normal visible-reply mode: automatic final reply
delivery when configured, or `message(action="send")` when the session requires
the message tool. If the requester session is inactive or its active wake
fails, and some generated media is still missing from the completion reply,
OpenClaw sends an idempotent direct fallback with only the missing media. Media
already delivered by the completion reply is not posted again.
## Speech-to-text and Voice Call

View File

@@ -15,12 +15,12 @@ fal, Google, MiniMax, and OpenRouter today.
For session-backed agent runs, OpenClaw starts music generation as a
background task, tracks it in the task ledger, then wakes the agent again
when the track is ready so the agent can tell the user and attach the
finished audio. Generated-media completions are delivered by the agent through
the message tool. If the requester session is inactive or its active wake
fails, and some generated audio is still missing from message-tool delivery,
OpenClaw sends an idempotent direct fallback with only the missing audio. The
completion wake explicitly warns the agent that normal final replies are
private for this route.
finished audio. The completion agent follows the session's normal visible-reply
mode: automatic final reply delivery when configured, or `message(action="send")`
when the session requires the message tool. If the requester session is
inactive or its active wake fails, and some generated audio is still missing
from the completion reply, OpenClaw sends an idempotent direct fallback with
only the missing audio.
<Note>
The built-in shared tool only appears when at least one music-generation

View File

@@ -153,6 +153,7 @@ Current source-of-truth:
- `/commands` shows the generated command catalog.
- `/tools [compact|verbose]` shows what the current agent can use right now.
- `/status` shows execution/runtime status, Gateway and system uptime, plus provider usage/quota when available.
- `/goal [status] | /goal start <objective> | /goal pause|resume|complete|block|clear` manages the current session's durable [goal](/tools/goal).
- `/diagnostics [note]` is the owner-only support-report flow for Gateway bugs and Codex harness runs. It asks for explicit exec approval every time before running `openclaw gateway diagnostics export --json`; do not approve diagnostics with an allow-all rule. After approval, it sends a pasteable report with the local bundle path, manifest summary, privacy notes, and relevant session ids. In group chats, the approval prompt and report go to the owner privately. When the active session uses the OpenAI Codex harness, the same approval also sends relevant Codex feedback to OpenAI servers and the completed reply lists the OpenClaw session ids, Codex thread ids, and `codex resume <thread-id>` commands. See [Diagnostics Export](/gateway/diagnostics).
- `/crestodian <request>` runs the Crestodian setup and repair helper from an owner DM.
- `/tasks` lists active/recent background tasks for the current session.

View File

@@ -1,13 +1,13 @@
---
summary: "Compact noisy exec and bash tool results with an optional bundled plugin"
summary: "Compact noisy exec and bash tool results with the optional Tokenjuice plugin"
title: "Tokenjuice"
read_when:
- You want shorter `exec` or `bash` tool results in OpenClaw
- You want to enable the bundled tokenjuice plugin
- You want to install or enable the Tokenjuice plugin
- You need to understand what tokenjuice changes and what it leaves raw
---
`tokenjuice` is an optional bundled plugin that compacts noisy `exec` and `bash`
`tokenjuice` is an optional external plugin that compacts noisy `exec` and `bash`
tool results after the command has already run.
It changes the returned `tool_result`, not the command itself. Tokenjuice does
@@ -19,7 +19,13 @@ trims the output before it goes back into the active harness session.
## Enable the plugin
Fast path:
Install once:
```bash
openclaw plugins install clawhub:@openclaw/tokenjuice
```
Then enable it:
```bash
openclaw config set plugins.entries.tokenjuice.enabled true
@@ -31,9 +37,6 @@ Equivalent:
openclaw plugins enable tokenjuice
```
OpenClaw already ships the plugin. There is no separate `plugins install`
or `tokenjuice install openclaw` step.
If you prefer editing config directly:
```json5

View File

@@ -62,10 +62,12 @@ session:
1. OpenClaw submits the request to the provider and immediately returns a task id.
2. The provider processes the job in the background (typically 30 seconds to several minutes depending on the provider and resolution; slow queue-backed providers can run up to the configured timeout).
3. When the video is ready, OpenClaw wakes the same session with an internal completion event.
4. The agent tells the user and attaches the finished video through the
message tool. If the requester session is inactive or its active wake
fails, and some generated video is still missing from message-tool delivery,
OpenClaw sends an idempotent direct fallback with only the missing video.
4. The agent tells the user through the session's normal visible-reply mode:
final reply delivery when automatic, or `message(action="send")` when the
session requires the message tool. If the requester session is inactive or
its active wake fails, and some generated video is still missing from the
completion reply, OpenClaw sends an idempotent direct fallback with only the
missing video.
While a job is in flight, duplicate `video_generate` calls in the same
session return the current task status instead of starting another

View File

@@ -54,7 +54,7 @@ Notes:
- Header: connection URL, current agent, current session.
- Chat log: user messages, assistant replies, system notices, tool cards.
- Status line: connection/run state (connecting, running, streaming, idle, error).
- Footer: connection state + agent + session + model + think/fast/verbose/trace/reasoning + token counts + deliver.
- Footer: connection state + agent + session + model + goal state + think/fast/verbose/trace/reasoning + token counts + deliver.
- Input: text editor with autocomplete.
## Mental model: agents + sessions
@@ -68,6 +68,9 @@ Notes:
- `per-sender` (default): each agent has many sessions.
- `global`: the TUI always uses the `global` session (the picker may be empty).
- The current agent + session are always visible in the footer.
- If the session has a [goal](/tools/goal), the footer shows its compact state
such as `Pursuing goal`, `Goal paused (/goal resume)`, or
`Goal achieved`.
- When started without `--session`, gateway-mode TUI resumes the last selected session for the same gateway, agent, and session scope if that session still exists. Passing `--session`, `/session`, `/new`, or `/reset` remains explicit.
## Sending + delivery
@@ -116,6 +119,7 @@ Session controls:
- `/trace <on|off>`
- `/reasoning <on|off|stream>`
- `/usage <off|tokens|full>`
- `/goal [status] | /goal start <objective> | /goal pause|resume|complete|block|clear`
- `/elevated <on|off|ask|full>` (alias: `/elev`)
- `/activation <mention|always>`
- `/deliver <on|off>`

33
extensions/acpx/README.md Normal file
View File

@@ -0,0 +1,33 @@
# @openclaw/acpx
Official ACP runtime backend for OpenClaw.
ACPx lets OpenClaw run external coding harnesses through the Agent Client Protocol while OpenClaw still owns sessions, channels, delivery, permissions, and Gateway state.
## Install
```bash
openclaw plugins install @openclaw/acpx
```
Restart the Gateway after installing or updating the plugin.
## What it provides
- ACP-backed agent runtime sessions.
- Plugin-owned session and transport management.
- MCP bridge helpers for OpenClaw tools and plugin tools.
- Static runtime assets used by the ACP process bridge.
## Configure
Use the ACP docs for harness-specific setup, permission modes, and model/runtime selection:
- https://docs.openclaw.ai/tools/acp-agents-setup
- https://docs.openclaw.ai/tools/acp-agents
## Package
- Plugin id: `acpx`
- Package: `@openclaw/acpx`
- Minimum OpenClaw host: `2026.4.25`

View File

@@ -17,29 +17,23 @@ vi.mock("./tts.js", async (importOriginal) => {
import { buildAzureSpeechProvider } from "./speech-provider.js";
describe("buildAzureSpeechProvider", () => {
const originalEnv = {
AZURE_SPEECH_KEY: process.env.AZURE_SPEECH_KEY,
AZURE_SPEECH_API_KEY: process.env.AZURE_SPEECH_API_KEY,
AZURE_SPEECH_REGION: process.env.AZURE_SPEECH_REGION,
AZURE_SPEECH_ENDPOINT: process.env.AZURE_SPEECH_ENDPOINT,
SPEECH_KEY: process.env.SPEECH_KEY,
SPEECH_REGION: process.env.SPEECH_REGION,
};
const envKeys = [
"AZURE_SPEECH_KEY",
"AZURE_SPEECH_API_KEY",
"AZURE_SPEECH_REGION",
"AZURE_SPEECH_ENDPOINT",
"SPEECH_KEY",
"SPEECH_REGION",
] as const;
beforeEach(() => {
for (const key of Object.keys(originalEnv)) {
delete process.env[key];
for (const key of envKeys) {
vi.stubEnv(key, undefined);
}
});
afterEach(() => {
for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
vi.unstubAllEnvs();
azureSpeechTTSMock.mockClear();
listAzureSpeechVoicesMock.mockClear();
vi.restoreAllMocks();
@@ -52,12 +46,6 @@ describe("buildAzureSpeechProvider", () => {
it("reports configured only when key plus region or endpoint is available", () => {
const provider = buildAzureSpeechProvider();
delete process.env.AZURE_SPEECH_KEY;
delete process.env.AZURE_SPEECH_API_KEY;
delete process.env.SPEECH_KEY;
delete process.env.AZURE_SPEECH_REGION;
delete process.env.SPEECH_REGION;
delete process.env.AZURE_SPEECH_ENDPOINT;
expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30_000 })).toBe(false);
expect(provider.isConfigured({ providerConfig: { apiKey: "key" }, timeoutMs: 30_000 })).toBe(
@@ -70,8 +58,8 @@ describe("buildAzureSpeechProvider", () => {
}),
).toBe(true);
process.env.AZURE_SPEECH_KEY = "env-key";
process.env.AZURE_SPEECH_REGION = "eastus";
vi.stubEnv("AZURE_SPEECH_KEY", "env-key");
vi.stubEnv("AZURE_SPEECH_REGION", "eastus");
expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30_000 })).toBe(true);
});

View File

@@ -0,0 +1,36 @@
# @openclaw/brave-plugin
Official Brave Search provider plugin for OpenClaw.
This plugin registers Brave as a `web_search` provider. It supports normal Brave web search and Brave LLM Context API mode.
## Install
```bash
openclaw plugins install @openclaw/brave-plugin
```
Restart the Gateway after installing or updating the plugin.
## Configure
Store a Brave Search API key in plugin config or expose `BRAVE_API_KEY` to the Gateway:
```bash
openclaw config set plugins.entries.brave.enabled true
openclaw config set tools.web.search.provider brave
```
Provider-specific options live under `plugins.entries.brave.config.webSearch.*`.
## Docs
Full setup, config examples, search modes, and tool parameters:
- https://docs.openclaw.ai/tools/brave-search
## Package
- Plugin id: `brave`
- Package: `@openclaw/brave-plugin`
- Minimum OpenClaw host: `2026.4.10`

View File

@@ -1,3 +1,4 @@
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import "../test-support/browser-security.mock.js";
import type { OpenClawConfig } from "../config/config.js";
@@ -576,6 +577,31 @@ describe("fetchBrowserJson loopback auth", () => {
);
});
it("caps oversized absolute HTTP timeouts before arming the watchdog", async () => {
const timeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockReturnValue(1 as unknown as ReturnType<typeof setTimeout>);
vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => undefined);
vi.stubGlobal(
"fetch",
vi.fn(async () => {
throw new Error("timed out");
}),
);
await expectThrownBrowserFetchError(
() =>
fetchBrowserJson<{ ok: boolean }>("http://example.com/", {
timeoutMs: Number.MAX_SAFE_INTEGER,
}),
{
contains: [`timed out after ${MAX_TIMER_TIMEOUT_MS}ms`],
omits: ["Do NOT retry the browser tool"],
},
);
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
});
it("omits no-retry hint for absolute HTTP abort failures", async () => {
vi.stubGlobal(
"fetch",

View File

@@ -1,5 +1,5 @@
import { parseBrowserHttpUrl } from "openclaw/plugin-sdk/browser-config";
import { parseFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
@@ -159,8 +159,7 @@ function appendBrowserToolModelHint(message: string): string {
type BrowserFetchFailureKind = "timeout" | "aborted" | "persistent";
function resolveBrowserFetchTimeoutMs(timeoutMs: number | undefined): number {
const parsed = parseFiniteNumber(timeoutMs);
return Math.max(1, Math.floor(parsed ?? 5000));
return resolveTimerTimeoutMs(timeoutMs, 5000);
}
function classifyBrowserFetchFailure(err: unknown): BrowserFetchFailureKind {

View File

@@ -75,9 +75,8 @@ describe("browser element commands", () => {
await delayProgram.parseAsync(["browser", "click-coords", "10", "20", "--delay-ms", "+0005"], {
from: "user",
});
const delayRequest = mocks.callBrowserRequest.mock.calls.at(-1)?.[1] as
| { body?: { delayMs?: number } }
| undefined;
const delayCall = mocks.callBrowserRequest.mock.calls.at(-1) as unknown[] | undefined;
const delayRequest = delayCall?.[1] as { body?: { delayMs?: number } } | undefined;
expect(delayRequest?.body?.delayMs).toBe(5);
const timeoutProgram = createElementProgram();
@@ -85,12 +84,9 @@ describe("browser element commands", () => {
["browser", "scrollintoview", "ref-1", "--timeout-ms", "+020000"],
{ from: "user" },
);
const timeoutRequest = mocks.callBrowserRequest.mock.calls.at(-1)?.[1] as
| { body?: { timeoutMs?: number } }
| undefined;
const timeoutOptions = mocks.callBrowserRequest.mock.calls.at(-1)?.[2] as
| { timeoutMs?: number }
| undefined;
const timeoutCall = mocks.callBrowserRequest.mock.calls.at(-1) as unknown[] | undefined;
const timeoutRequest = timeoutCall?.[1] as { body?: { timeoutMs?: number } } | undefined;
const timeoutOptions = timeoutCall?.[2] as { timeoutMs?: number } | undefined;
expect(timeoutRequest?.body?.timeoutMs).toBe(20_000);
expect(timeoutOptions?.timeoutMs).toBeGreaterThan(20_000);
});

View File

@@ -0,0 +1,18 @@
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { describe, expect, it, vi } from "vitest";
import { withTimeout } from "./sdk-node-runtime.js";
describe("withTimeout", () => {
it("caps oversized timeouts before arming the abort timer", async () => {
const timeoutSpy = vi
.spyOn(globalThis, "setTimeout")
.mockReturnValue(1 as unknown as ReturnType<typeof setTimeout>);
vi.spyOn(globalThis, "clearTimeout").mockImplementation(() => undefined);
await expect(
withTimeout(async () => "ok", Number.MAX_SAFE_INTEGER, "browser request"),
).resolves.toBe("ok");
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
});
});

View File

@@ -23,11 +23,10 @@ export {
type LazyPluginServiceHandle,
} from "openclaw/plugin-sdk/plugin-runtime";
export { defaultRuntime } from "openclaw/plugin-sdk/runtime-env";
import { clampTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
function normalizeTimeoutMs(timeoutMs: number | undefined): number | undefined {
return typeof timeoutMs === "number" && Number.isFinite(timeoutMs)
? Math.max(1, Math.floor(timeoutMs))
: undefined;
return clampTimerTimeoutMs(timeoutMs);
}
function createTimeoutAbortSignal(timeoutMs: number, label: string | undefined) {
@@ -38,7 +37,10 @@ function createTimeoutAbortSignal(timeoutMs: number, label: string | undefined)
return { controller, error, timer };
}
function waitForAbort(signal: AbortSignal, fallback: Error): {
function waitForAbort(
signal: AbortSignal,
fallback: Error,
): {
promise: Promise<never>;
cleanup: () => void;
} {

View File

@@ -264,6 +264,7 @@ function websocketMessageToString(data: WebSocket.RawData): string {
class WebSocketCodexJsonRpcConnection extends BaseCodexJsonRpcConnection {
private readonly ws: WebSocket;
private readonly openPromise: Promise<void>;
private closing = false;
constructor(endpoint: Extract<CodexSupervisorEndpoint, { transport: "websocket" }>) {
super();
@@ -294,7 +295,11 @@ class WebSocketCodexJsonRpcConnection extends BaseCodexJsonRpcConnection {
}
});
this.ws.once("error", (error) => this.fail(error));
this.ws.once("close", () => this.fail(new Error("Codex app-server websocket closed")));
this.ws.once("close", () => {
if (!this.closing) {
this.fail(new Error("Codex app-server websocket closed"));
}
});
}
async ready(): Promise<void> {
@@ -310,7 +315,27 @@ class WebSocketCodexJsonRpcConnection extends BaseCodexJsonRpcConnection {
}
async close(): Promise<void> {
this.ws.close();
this.closing = true;
this.fail(new Error("Codex app-server websocket closed"));
if (this.ws.readyState === WebSocket.CLOSED) {
return;
}
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
this.ws.terminate();
resolve();
}, 1000);
this.ws.once("close", () => {
clearTimeout(timeout);
resolve();
});
if (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN) {
this.ws.close();
} else {
clearTimeout(timeout);
resolve();
}
});
}
}

View File

@@ -159,10 +159,12 @@ export function registerCodexSupervisorMcpTools(
"List Codex sessions visible to the OpenClaw supervisor.",
{
include_stored: z.boolean().optional(),
max_stored_sessions: z.number().int().min(1).max(1000).optional(),
},
async ({ include_stored }) => {
async ({ include_stored, max_stored_sessions }) => {
const result = await supervisor.listSessionSnapshot({
includeStored: include_stored ?? false,
maxStoredSessions: max_stored_sessions,
});
return textResult(
`codex sessions: ${result.sessions.length}`,

View File

@@ -110,6 +110,40 @@ describe("createCodexSupervisorTools", () => {
).rejects.toThrow("Codex write controls are disabled");
});
it("rejects stored session limits outside the runtime bounds", async () => {
const { supervisor } = createSupervisorStub();
const tools = createCodexSupervisorTools({
supervisor,
policy: { allowRawTranscripts: false, allowWriteControls: false },
});
await expect(
toolByName(tools, "codex_sessions_list").execute("call-1", {
include_stored: true,
max_stored_sessions: "2",
}),
).rejects.toThrow("max_stored_sessions must be an integer");
await expect(
toolByName(tools, "codex_sessions_list").execute("call-2", {
include_stored: true,
max_stored_sessions: 1001,
}),
).rejects.toThrow("max_stored_sessions must be between 1 and 1000");
await expect(
toolByName(tools, "codex_sessions_list").execute("call-2", {
include_stored: true,
max_stored_sessions: null,
}),
).rejects.toThrow("max_stored_sessions must be an integer");
await expect(
toolByName(tools, "codex_sessions_list").execute("call-3", {
include_stored: true,
max_stored_sessions: Number.MAX_SAFE_INTEGER + 1,
}),
).rejects.toThrow("max_stored_sessions must be between 1 and 1000");
});
it("allows trusted read and write tools when policy enables them", async () => {
const { calls, supervisor } = createSupervisorStub();
const tools = createCodexSupervisorTools({

View File

@@ -1,4 +1,5 @@
import { jsonResult, readStringParam, type AnyAgentTool } from "openclaw/plugin-sdk/core";
import { asSafeIntegerInRange } from "openclaw/plugin-sdk/number-runtime";
import { Type } from "typebox";
import {
redactCodexSupervisorEndpoint,
@@ -13,6 +14,7 @@ const EmptyParamsSchema = Type.Object({}, { additionalProperties: false });
const SessionsListParamsSchema = Type.Object(
{
include_stored: Type.Optional(Type.Boolean()),
max_stored_sessions: Type.Optional(Type.Integer({ minimum: 1, maximum: 1000 })),
},
{ additionalProperties: false },
);
@@ -67,6 +69,21 @@ function readBooleanParam(params: Record<string, unknown>, key: string): boolean
return params[key] === true;
}
function readIntegerParam(params: Record<string, unknown>, key: string): number | undefined {
const value = params[key];
if (value === undefined) {
return undefined;
}
const integer = asSafeIntegerInRange(value, { min: 1, max: 1000 });
if (integer === undefined) {
if (typeof value === "number" && Number.isInteger(value)) {
throw new Error(`${key} must be between 1 and 1000`);
}
throw new Error(`${key} must be an integer`);
}
return integer;
}
function readModeParam(params: Record<string, unknown>): CodexSupervisorTurnMode | undefined {
const mode = readStringParam(params, "mode");
if (!mode) {
@@ -122,6 +139,7 @@ export function createCodexSupervisorTools({
const params = asRecord(rawParams);
const result = await supervisor.listSessionSnapshot({
includeStored: readBooleanParam(params, "include_stored"),
maxStoredSessions: readIntegerParam(params, "max_stored_sessions"),
});
return jsonResult({
summary: `codex sessions: ${result.sessions.length}`,

View File

@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { describe, expect, it } from "vitest";
import { WebSocketServer } from "ws";
import { loadCodexSupervisorEndpoints, resolveCodexSupervisorPluginConfig } from "./config.js";
import { connectCodexAppServerEndpoint, resolveSafeApprovalResult } from "./json-rpc-client.js";
import { CodexSupervisor } from "./supervisor.js";
@@ -350,6 +351,7 @@ describe("CodexSupervisor", () => {
]);
expect(fake.calls.find((call) => call.method === "thread/list")?.params).toMatchObject({
sourceKinds: ["cli", "vscode", "exec", "appServer", "unknown"],
useStateDbOnly: true,
});
});
@@ -394,6 +396,51 @@ describe("CodexSupervisor", () => {
]);
});
it("bounds stored session pagination for large real Codex homes", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
if (method === "thread/list") {
return {
data: [
{ id: "thread-1", status: { type: "notLoaded" }, turns: [] },
{ id: "thread-2", status: { type: "notLoaded" }, turns: [] },
],
nextCursor: "page-2",
};
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(
supervisor.listSessions({ includeStored: true, maxStoredSessions: 1 }),
).resolves.toEqual([
{
endpointId: "local",
threadId: "thread-1",
status: "notLoaded",
},
]);
expect(fake.calls.filter((call) => call.method === "thread/list")).toEqual([
{
method: "thread/list",
params: {
limit: 1,
sourceKinds: ["cli", "vscode", "exec", "appServer", "unknown"],
useStateDbOnly: true,
},
},
]);
});
it("closes settled connections when evicting them", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
@@ -508,6 +555,88 @@ describe("CodexSupervisor", () => {
});
});
it("uses a unique loaded endpoint match even when another endpoint is down", async () => {
const upEndpoint: CodexSupervisorEndpoint = { id: "up", transport: "stdio-proxy" };
const downEndpoint: CodexSupervisorEndpoint = { id: "down", transport: "stdio-proxy" };
const fake = new FakeCodexConnection({
id: "thread-1",
status: { type: "idle" },
turns: [],
});
const supervisor = new CodexSupervisor([upEndpoint, downEndpoint], async (target) => {
if (target.id === "down") {
throw new Error("host offline");
}
return fake;
});
await expect(
supervisor.sendToSession({ threadId: "thread-1", text: "continue" }),
).resolves.toMatchObject({
endpointId: "up",
threadId: "thread-1",
mode: "start",
});
});
it("resolves omitted endpoint ids by exact thread read without scanning stored pages", async () => {
const fake = new FakeCodexConnection({
id: "thread-old",
status: { type: "notLoaded" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
if (method === "thread/read" && params?.threadId === "thread-old") {
return { thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] } };
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([endpoint], async () => fake);
await expect(supervisor.readSession({ threadId: "thread-old" })).resolves.toEqual({
thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] },
});
expect(fake.calls.map((call) => call.method)).toEqual([
"thread/loaded/list",
"thread/read",
"thread/read",
]);
});
it("resolves stored threads on healthy endpoints when another endpoint is down", async () => {
const downEndpoint: CodexSupervisorEndpoint = { id: "down", transport: "stdio-proxy" };
const upEndpoint: CodexSupervisorEndpoint = { id: "up", transport: "stdio-proxy" };
const fake = new FakeCodexConnection({
id: "thread-old",
status: { type: "notLoaded" },
turns: [],
});
fake.request = async (method, params) => {
fake.calls.push({ method, params });
if (method === "thread/loaded/list") {
return { data: [], nextCursor: null };
}
if (method === "thread/read" && params?.threadId === "thread-old") {
return { thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] } };
}
throw new Error(`unexpected method: ${method}`);
};
const supervisor = new CodexSupervisor([downEndpoint, upEndpoint], async (target) => {
if (target.id === "down") {
throw new Error("host offline");
}
return fake;
});
await expect(supervisor.readSession({ threadId: "thread-old" })).resolves.toEqual({
thread: { id: "thread-old", status: { type: "notLoaded" }, turns: [] },
});
});
it("steers active sessions when the in-progress turn is readable", async () => {
const fake = new FakeCodexConnection({
id: "thread-1",
@@ -668,6 +797,53 @@ async function waitForFile(filePath: string): Promise<string> {
}
describe("connectCodexAppServerEndpoint", () => {
it("rejects pending websocket requests when the supervisor closes intentionally", async () => {
const server = new WebSocketServer({ host: "127.0.0.1", port: 0 });
const port = await new Promise<number>((resolve) => {
server.once("listening", () => {
const address = server.address();
resolve(typeof address === "object" && address ? address.port : 0);
});
});
const sawProbeRequest = new Promise<void>((resolve) => {
server.once("connection", (socket) => {
socket.on("message", (data) => {
const messageText =
typeof data === "string"
? data
: Array.isArray(data)
? Buffer.concat(data).toString()
: data instanceof ArrayBuffer
? Buffer.from(new Uint8Array(data)).toString()
: Buffer.from(data).toString();
const request = JSON.parse(messageText) as Record<string, unknown>;
if (request.method === "initialize") {
socket.send(JSON.stringify({ id: request.id, result: {} }));
}
if (request.method === "thread/loaded/list") {
resolve();
}
});
});
});
const supervisor = new CodexSupervisor(
[{ id: "ws", transport: "websocket", url: `ws://127.0.0.1:${port}` }],
connectCodexAppServerEndpoint,
);
const probe = supervisor.probeEndpoints();
await sawProbeRequest;
await supervisor.close();
await expect(
Promise.race([
probe,
new Promise((_, reject) => setTimeout(() => reject(new Error("probe timed out")), 500)),
]),
).resolves.toMatchObject([{ endpointId: "ws", ok: false }]);
await new Promise<void>((resolve) => server.close(() => resolve()));
});
it("rejects malformed stdio frames instead of throwing out of band", async () => {
const markerDir = await fs.mkdtemp(path.join(os.tmpdir(), "codex-supervisor-malformed-"));
const marker = path.join(markerDir, "closed");
@@ -735,7 +911,7 @@ describe("connectCodexAppServerEndpoint", () => {
process.stdout.write(JSON.stringify({ id: request.id, result: {} }) + "\\n");
return;
}
if (request.method === "thread/list") {
if (request.method === "thread/loaded/list") {
process.stdout.write(JSON.stringify({ id: request.id, result: { threads: [] } }) + "\\n");
setTimeout(() => process.exit(0), 0);
}

View File

@@ -13,6 +13,7 @@ import type {
type EndpointConnector = (endpoint: CodexSupervisorEndpoint) => Promise<CodexJsonRpcConnection>;
const ALL_CODEX_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer", "unknown"];
const DEFAULT_MAX_STORED_SESSIONS = 200;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -137,7 +138,7 @@ export class CodexSupervisor {
this.endpoints.map(async (endpoint) => {
try {
const connection = await this.connectionFor(endpoint.id);
await connection.request("thread/list", { limit: 1 });
await connection.request("thread/loaded/list", { limit: 1 });
return { endpointId: endpoint.id, ok: true };
} catch (error) {
this.forgetEndpoint(endpoint.id);
@@ -151,12 +152,14 @@ export class CodexSupervisor {
);
}
async listSessions(params: { includeStored?: boolean } = {}): Promise<CodexSupervisorSession[]> {
async listSessions(
params: { includeStored?: boolean; maxStoredSessions?: number } = {},
): Promise<CodexSupervisorSession[]> {
return (await this.listSessionSnapshot(params)).sessions;
}
async listSessionSnapshot(
params: { includeStored?: boolean } = {},
params: { includeStored?: boolean; maxStoredSessions?: number } = {},
): Promise<CodexSupervisorSessionListResult> {
const sessions: CodexSupervisorSession[] = [];
const errors: CodexSupervisorEndpointHealth[] = [];
@@ -273,12 +276,15 @@ export class CodexSupervisor {
private async listEndpointSessions(
endpoint: CodexSupervisorEndpoint,
params: { includeStored?: boolean },
params: { includeStored?: boolean; maxStoredSessions?: number },
): Promise<CodexSupervisorSession[]> {
if (params.includeStored === true) {
const loaded = await this.listLoadedThreadSessions(endpoint);
const sessions = [...loaded];
for (const stored of await this.listStoredThreadSessions(endpoint)) {
for (const stored of await this.listStoredThreadSessions(
endpoint,
params.maxStoredSessions,
)) {
if (!sessions.some((session) => session.threadId === stored.threadId)) {
sessions.push(stored);
}
@@ -318,14 +324,23 @@ export class CodexSupervisor {
private async listStoredThreadSessions(
endpoint: CodexSupervisorEndpoint,
maxStoredSessions = DEFAULT_MAX_STORED_SESSIONS,
): Promise<CodexSupervisorSession[]> {
const sessionLimit = Number.isFinite(maxStoredSessions)
? Math.min(1000, Math.max(1, Math.floor(maxStoredSessions)))
: DEFAULT_MAX_STORED_SESSIONS;
const sessions: CodexSupervisorSession[] = [];
const connection = await this.connectionFor(endpoint.id);
let cursor: string | undefined;
do {
const remaining = sessionLimit - sessions.length;
if (remaining <= 0) {
break;
}
const listed = await connection.request("thread/list", {
limit: 100,
limit: Math.min(100, remaining),
sourceKinds: ALL_CODEX_THREAD_SOURCE_KINDS,
useStateDbOnly: true,
...(cursor ? { cursor } : {}),
});
for (const thread of extractThreadList(listed)) {
@@ -340,6 +355,9 @@ export class CodexSupervisor {
const session = toSession(endpoint.id, thread);
if (session) {
sessions.push(session);
if (sessions.length >= sessionLimit) {
break;
}
}
}
cursor =
@@ -435,7 +453,7 @@ export class CodexSupervisor {
if (params.endpointId) {
return params.endpointId;
}
const sessions = await this.listSessions({ includeStored: true });
const sessions = await this.listSessions();
const matches = sessions.filter((session) => session.threadId === params.threadId);
if (matches.length === 1) {
return matches[0].endpointId;
@@ -443,6 +461,34 @@ export class CodexSupervisor {
if (matches.length > 1) {
throw new Error(`Codex thread id is ambiguous across endpoints: ${params.threadId}`);
}
const endpointIds = new Set(matches.map((match) => match.endpointId));
for (const endpoint of this.endpoints) {
if (endpointIds.has(endpoint.id)) {
continue;
}
try {
const connection = await this.connectionFor(endpoint.id);
const read = await this.readThread(connection, params.threadId, false);
const thread = extractThread(read);
if (thread?.id === params.threadId) {
endpointIds.add(endpoint.id);
}
} catch (error) {
if (isLoadedThreadReadMiss(error)) {
continue;
}
this.forgetEndpoint(endpoint.id);
continue;
}
}
if (endpointIds.size === 1) {
for (const endpointId of endpointIds) {
return endpointId;
}
}
if (endpointIds.size > 1) {
throw new Error(`Codex thread id is ambiguous across endpoints: ${params.threadId}`);
}
throw new Error(`Codex thread not found: ${params.threadId}`);
}

View File

@@ -176,7 +176,8 @@
},
"postToolRawAssistantCompletionIdleTimeoutMs": {
"type": "number",
"minimum": 1
"minimum": 1,
"default": 300000
},
"approvalPolicy": {
"type": "string",
@@ -360,7 +361,7 @@
},
"appServer.postToolRawAssistantCompletionIdleTimeoutMs": {
"label": "Post-Tool Raw Assistant Completion Idle Timeout",
"help": "Completion-idle guard after a tool handoff when Codex emits raw assistant completion or progress without turn/completed. Defaults to the assistant completion idle timeout when unset.",
"help": "Completion-idle guard after a tool handoff when Codex emits raw assistant completion or progress without turn/completed. Defaults to 300000 ms when unset.",
"advanced": true
},
"appServer.approvalPolicy": {

View File

@@ -4,6 +4,7 @@ import {
isAssistantCompletionReleaseNotification,
isCodexTurnAbortMarkerNotification,
isNativeToolProgressNotification,
isNativeResponseStreamDeltaNotification,
isPendingOpenClawDynamicToolCompletionNotification,
isRawAssistantCompletionNotification,
isRawReasoningCompletionNotification,
@@ -99,9 +100,10 @@ export function applyCodexTurnNotificationState(params: {
params.turnId,
);
const isTurnCompletion = notification.method === "turn/completed" && isCurrentTurnNotification;
const isNativeResponseStreamDelta = isNativeResponseStreamDeltaNotification(notification);
let turnCrossedToolHandoff = params.turnCrossedToolHandoff;
if (isCurrentTurnNotification) {
if (isCurrentTurnNotification && !isNativeResponseStreamDelta) {
turnWatches.touchActivity(`notification:${notification.method}`, {
details: describeNotificationActivity(notification),
attemptProgress: true,
@@ -174,6 +176,9 @@ export function applyCodexTurnNotificationState(params: {
} else if (isCurrentTurnNotification && assistantCompletionCanRelease) {
turnWatches.armAssistantCompletionIdleWatch(describeNotificationActivity(notification));
} else if (postToolRawAssistantCompletionNeedsTerminalGuard) {
// A post-tool assistant status can be followed by native Codex streaming a
// large custom tool input. Forwarded raw deltas refresh activity at enqueue
// time; keep this guard conservative for versions that do not forward them.
turnWatches.armCompletionIdleWatch({
timeoutMs: params.postToolRawAssistantCompletionIdleTimeoutMs,
});
@@ -203,6 +208,7 @@ export function applyCodexTurnNotificationState(params: {
!turnWatches.isCompletionIdleWatchPinnedByTerminalError() &&
notification.method !== "turn/completed" &&
isCurrentTurnNotification &&
!isNativeResponseStreamDelta &&
!trackedDynamicToolCompletion &&
!rawToolOutputCompletion &&
!postToolRawAssistantCompletionNeedsTerminalGuard &&

View File

@@ -179,6 +179,12 @@ export function isNativeToolProgressNotification(notification: CodexServerNotifi
}
}
export function isNativeResponseStreamDeltaNotification(
notification: CodexServerNotification,
): boolean {
return notification.method.startsWith("response.") && notification.method.endsWith(".delta");
}
export function isRawAssistantCompletionNotification(
notification: CodexServerNotification,
): boolean {

View File

@@ -89,6 +89,28 @@ describe("Codex app-server attempt results", () => {
replayInvalid: true,
livenessState: "abandoned",
});
expect(
buildCodexAppServerPromptTimeoutOutcome({
result: createResult({
assistantTexts: ["I am changing the data model now..."],
}),
turnCompletionIdleTimedOut: true,
}),
).toEqual({
message:
"Codex stopped before confirming the turn was complete. The response may be incomplete; retry if needed.",
});
expect(
buildCodexAppServerPromptTimeoutOutcome({
result: createResult({
toolMetas: [{ toolName: "exec" }],
}),
turnCompletionIdleTimedOut: true,
}),
).toEqual({
message:
"Codex stopped before confirming the turn was complete. The response may be incomplete; retry if needed.",
});
});
it("classifies replay blocked reasons", () => {

View File

@@ -27,10 +27,12 @@ export function buildCodexAppServerPromptTimeoutOutcome(params: {
const completionIdleTimeoutHadPotentialSideEffects = hasCodexAppServerPotentialSideEffectEvidence(
params.result,
);
const replayBlockedReason = resolveCodexAppServerReplayBlockedReason(params.result);
if (
!params.turnCompletionIdleTimedOut ||
(params.result.itemLifecycle.completedCount === 0 &&
!completionIdleTimeoutHadPotentialSideEffects)
!completionIdleTimeoutHadPotentialSideEffects &&
replayBlockedReason === undefined)
) {
return undefined;
}

View File

@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS,
CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS,
CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS,
CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS,
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
@@ -36,6 +37,11 @@ describe("Codex app-server attempt timeouts", () => {
});
it("normalizes turn idle timeout overrides", () => {
expect(CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS).toBe(5 * 60_000);
expect(CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS).toBeGreaterThan(
CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS,
);
expect(resolveCodexTurnCompletionIdleTimeoutMs(undefined)).toBe(
CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS,
);
@@ -54,9 +60,21 @@ describe("Codex app-server attempt timeouts", () => {
expect(resolveCodexTurnAssistantCompletionIdleTimeoutMs(9.8)).toBe(9);
expect(resolveCodexTurnAssistantCompletionIdleTimeoutMs(-10)).toBe(1);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(undefined, 123)).toBe(123);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(Number.NaN, 123)).toBe(123);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(undefined, Number.NaN)).toBe(1);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(undefined, 123)).toBe(
CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS,
);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(Number.NaN, 123)).toBe(
CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS,
);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(undefined, 120_000)).toBe(
CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS,
);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(undefined, 6 * 60_000)).toBe(
6 * 60_000,
);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(undefined, Number.NaN)).toBe(
CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS,
);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(7.9, 123)).toBe(7);
expect(resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(0, 123)).toBe(1);

View File

@@ -3,6 +3,10 @@ import { parseFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
export const CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS = 100;
export const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 60_000;
export const CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 10_000;
// Native Codex can stream a large custom tool input after a raw assistant
// progress item. Forwarded deltas count as activity, but older native paths may
// not surface them, so keep this terminal guard conservative.
export const CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 5 * 60_000;
export const CODEX_POST_REASONING_SOURCE_REPLY_IDLE_TIMEOUT_MS = 5 * 60_000;
export const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 60_000;
@@ -91,7 +95,11 @@ export function resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(
value: number | undefined,
fallbackMs: number,
): number {
return resolvePositiveIntegerTimeoutMs(value, fallbackMs);
const defaultMs = Math.max(
resolvePositiveIntegerTimeoutMs(undefined, fallbackMs),
CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS,
);
return resolvePositiveIntegerTimeoutMs(value, defaultMs);
}
export function resolveCodexTurnTerminalIdleTimeoutMs(value: number | undefined): number {

View File

@@ -380,9 +380,21 @@ export function createCodexAttemptTurnWatchController(params: {
}
scheduleProgressWatches();
},
noteNotificationReceived: (method: string) => {
noteNotificationReceived: (
method: string,
options?: { details?: Record<string, unknown>; attemptProgress?: boolean },
) => {
completionLastActivityAt = Date.now();
completionLastActivityReason = `notification:${method}`;
if (options?.details !== undefined) {
completionLastActivityDetails = options.details;
}
if (options?.attemptProgress) {
attemptLastProgressAt = completionLastActivityAt;
attemptLastProgressReason = completionLastActivityReason;
attemptLastProgressDetails = options.details;
params.onAttemptProgress(completionLastActivityReason, options.details);
}
},
scheduleProgressWatches,
clearCompletionIdleTimer,

View File

@@ -102,6 +102,7 @@ export class CodexAppServerClient {
private readonly requestHandlers = new Set<CodexServerRequestHandler>();
private readonly notificationHandlers = new Set<CodexServerNotificationHandler>();
private readonly closeHandlers = new Set<(client: CodexAppServerClient) => void>();
private activeSharedLeaseCountProvider: (() => number | undefined) | undefined;
private nextId = 1;
private initialized = false;
private closed = false;
@@ -281,6 +282,16 @@ export class CodexAppServerClient {
return () => this.notificationHandlers.delete(handler);
}
setActiveSharedLeaseCountProviderForUnscopedNotifications(
provider: (() => number | undefined) | undefined,
): void {
this.activeSharedLeaseCountProvider = provider;
}
getActiveSharedLeaseCountForUnscopedNotifications(): number | undefined {
return this.activeSharedLeaseCountProvider?.();
}
addCloseHandler(handler: (client: CodexAppServerClient) => void): () => void {
this.closeHandlers.add(handler);
return () => this.closeHandlers.delete(handler);

View File

@@ -80,6 +80,7 @@ import {
isCurrentApprovalTurnRequestParams,
isCurrentThreadOptionalTurnRequestParams,
isCurrentThreadTurnRequestParams,
isNativeResponseStreamDeltaNotification,
isTerminalTurnStatus,
} from "./attempt-notifications.js";
import {
@@ -99,7 +100,10 @@ import {
resolveCodexTurnTerminalIdleTimeoutMs,
withCodexStartupTimeout,
} from "./attempt-timeouts.js";
import { createCodexAttemptTurnWatchController } from "./attempt-turn-watches.js";
import {
createCodexAttemptTurnWatchController,
type CodexAttemptTurnWatchTimeoutKind,
} from "./attempt-turn-watches.js";
import {
refreshCodexAppServerAuthTokens,
resolveCodexAppServerAuthAccountCacheKey,
@@ -202,6 +206,7 @@ import {
import { releaseCodexSandboxExecServerEnvironment } from "./sandbox-exec-server.js";
import {
clearCodexAppServerBinding,
clearCodexAppServerBindingForThread,
readCodexAppServerBinding,
type CodexAppServerThreadBinding,
} from "./session-binding.js";
@@ -941,6 +946,7 @@ export async function runCodexAppServerAttempt(
let terminalTurnNotificationQueued = false;
let timedOut = false;
let turnCompletionIdleTimedOut = false;
let turnWatchTimeoutKind: CodexAttemptTurnWatchTimeoutKind | undefined;
let turnCompletionIdleTimeoutMessage: string | undefined;
let clientClosedPromptError: string | undefined;
let clientClosedAbort = false;
@@ -1019,9 +1025,10 @@ export async function runCodexAppServerAttempt(
turnTerminalIdleTimeoutMs,
interruptTimeoutMs: CODEX_APP_SERVER_INTERRUPT_TIMEOUT_MS,
onInterruptTurn: (input) => interruptCodexTurnBestEffort(client, input),
onTimeout: () => {
onTimeout: (timeout) => {
timedOut = true;
turnCompletionIdleTimedOut = true;
turnWatchTimeoutKind = timeout.kind;
turnCompletionIdleTimeoutMessage =
"codex app-server turn idle timed out waiting for turn/completed";
},
@@ -1276,8 +1283,29 @@ export async function runCodexAppServerAttempt(
// Touch idle-watch timestamps at receive time, not just after queued
// projection. A queued terminal event should suppress short false-idle
// guards, while the full attempt watchdog still releases a wedged queue.
if (correlation.matchesActiveTurn !== false) {
turnWatches.noteNotificationReceived(notification.method);
const isNativeResponseStreamDelta = isNativeResponseStreamDeltaNotification(notification);
const nativeResponseStreamDeltaMatchesActiveTurn =
isNativeResponseStreamDelta &&
(correlation.matchesActiveTurn === true ||
(isUnscopedCodexNotification(correlation) &&
canAttributeUnscopedNativeResponseDeltaToThisTurn(client)));
const notificationMatchesActiveTurn =
correlation.matchesActiveTurn === true ||
(!isNativeResponseStreamDelta && correlation.matchesActiveTurn !== false) ||
nativeResponseStreamDeltaMatchesActiveTurn;
if (notificationMatchesActiveTurn) {
// If a future Codex app-server exposes raw response deltas, treat them as
// activity only when scoped to this turn or attributable to a single lease.
// Today the durable app-server raw-event surface is rawResponseItem/completed.
turnWatches.noteNotificationReceived(
notification.method,
isNativeResponseStreamDelta
? {
attemptProgress: true,
details: { lastNotificationMethod: notification.method },
}
: undefined,
);
}
notificationQueue = notificationQueue.then(
() => handleNotification(notification),
@@ -1881,11 +1909,15 @@ export async function runCodexAppServerAttempt(
const abortListener = () => {
const shouldRetireClient = timedOut;
if (shouldRetireClient) {
void retireCodexAppServerClientAfterTimedOutTurn(client, {
threadId: thread.threadId,
turnId: activeTurnId,
reason: String(runAbortController.signal.reason ?? "timeout"),
}).finally(() => {
void (async () => {
// Timed-out native turns cannot be safely resumed on the same thread.
await clearCodexAppServerBindingForThread(activeSessionFile, thread.threadId);
await retireCodexAppServerClientAfterTimedOutTurn(client, {
threadId: thread.threadId,
turnId: activeTurnId,
reason: String(runAbortController.signal.reason ?? "timeout"),
});
})().finally(() => {
resolveCompletion?.();
});
return;
@@ -2095,6 +2127,10 @@ export async function runCodexAppServerAttempt(
? {
codexAppServerFailure: {
kind: codexAppServerFailureKind,
...(codexAppServerFailureKind === "turn_completion_idle_timeout" &&
turnWatchTimeoutKind
? { turnWatchTimeoutKind }
: {}),
transport: appServer.start.transport,
threadId: thread.threadId,
turnId: activeTurnId,
@@ -2202,6 +2238,22 @@ function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}
function canAttributeUnscopedNativeResponseDeltaToThisTurn(client: CodexAppServerClient): boolean {
const activeLeases = client.getActiveSharedLeaseCountForUnscopedNotifications?.();
return activeLeases === undefined || activeLeases <= 1;
}
function isUnscopedCodexNotification(
correlation: ReturnType<typeof describeCodexNotificationCorrelation>,
): boolean {
return (
!correlation.threadId &&
!correlation.turnId &&
!correlation.nestedTurnThreadId &&
!correlation.nestedTurnId
);
}
function shouldRetryContextEngineTurnOnFreshCodexThread(params: {
error: unknown;
contextEngineActive: boolean;

View File

@@ -1,3 +1,4 @@
import fs from "node:fs/promises";
import path from "node:path";
import {
embeddedAgentLog,
@@ -31,6 +32,7 @@ import {
turnStartResult,
} from "./run-attempt-test-harness.js";
import { testing } from "./run-attempt.js";
import { resolveCodexAppServerBindingPath } from "./session-binding.js";
setupRunAttemptTestHooks();
@@ -70,6 +72,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
path.join(tempDir, "workspace"),
);
params.timeoutMs = 200;
const bindingPath = resolveCodexAppServerBindingPath(params.sessionFile);
const run = runCodexAppServerAttempt(params, {
pluginConfig: { appServer: { turnCompletionIdleTimeoutMs: 5 } },
@@ -115,6 +118,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
),
{ interval: 1 },
);
await expect(fs.stat(bindingPath)).rejects.toMatchObject({ code: "ENOENT" });
expect(queueActiveRunMessageForTest("session-1", "after timeout")).toBe(false);
});
@@ -1124,7 +1128,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
expect(request.mock.calls.some(([method]) => method === "turn/interrupt")).toBe(false);
});
it("times out post-tool raw assistant progress after the assistant idle timeout", async () => {
it("times out post-tool raw assistant progress after the post-tool timeout", async () => {
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
let handleRequest:
| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)
@@ -1167,6 +1171,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
const run = runCodexAppServerAttempt(params, {
turnCompletionIdleTimeoutMs: 50,
turnAssistantCompletionIdleTimeoutMs: 5,
postToolRawAssistantCompletionIdleTimeoutMs: 5,
turnTerminalIdleTimeoutMs: 500,
});
await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function"), fastWait);
@@ -1331,7 +1336,317 @@ describe("runCodexAppServerAttempt turn watches", () => {
expect(completionWarnData?.lastActivityReason).toBe("notification:rawResponseItem/completed");
});
it("times out post-native-tool raw assistant progress after the assistant idle timeout", async () => {
it("counts native response deltas as post-tool raw assistant activity", async () => {
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
let handleRequest:
| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)
| undefined;
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
return threadStartResult("thread-1");
}
if (method === "turn/start") {
return turnStartResult("turn-1", "inProgress");
}
return {};
});
setCodexAppServerClientFactoryForTest(
async () =>
({
request,
addNotificationHandler: (handler: typeof notify) => {
notify = handler;
return () => undefined;
},
addRequestHandler: (
handler: (request: {
id: string;
method: string;
params?: unknown;
}) => Promise<unknown>,
) => {
handleRequest = handler;
return () => undefined;
},
}) as never,
);
const params = createParams(
path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"),
);
params.timeoutMs = 60_000;
let settled = false;
const run = runCodexAppServerAttempt(params, {
turnCompletionIdleTimeoutMs: 500,
turnAssistantCompletionIdleTimeoutMs: 5,
postToolRawAssistantCompletionIdleTimeoutMs: 50,
turnTerminalIdleTimeoutMs: 500,
}).finally(() => {
settled = true;
});
await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function"), fastWait);
const toolResult = (await handleRequest?.({
id: "request-tool-1",
method: "item/tool/call",
params: {
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "message",
arguments: { action: "send", text: "already sent" },
},
})) as { success?: boolean };
expect(toolResult.success).toBe(false);
await notify({
method: "rawResponseItem/completed",
params: {
threadId: "thread-1",
turnId: "turn-1",
item: {
type: "message",
id: "raw-status-1",
role: "assistant",
content: [{ type: "output_text", text: "I'm writing a large patch now." }],
},
},
});
await new Promise((resolve) => setTimeout(resolve, 30));
// This covers the future-compatible path for raw response deltas if Codex
// app-server exposes them directly; current Codex primarily emits
// rawResponseItem/completed for the raw-event surface.
await notify({
method: "response.custom_tool_call_input.delta",
params: {
item_id: "ctc-large-edit-1",
output_index: 0,
delta: '{"cmd":"apply_patch","patch":"large chunk"}',
},
});
await new Promise((resolve) => setTimeout(resolve, 30));
expect(settled).toBe(false);
await notify({
method: "turn/completed",
params: {
threadId: "thread-1",
turnId: "turn-1",
turn: { id: "turn-1", status: "completed" },
},
});
const result = await run;
expect(result.aborted).toBe(false);
expect(result.timedOut).toBe(false);
expect(result.promptError).toBeNull();
});
it("keeps the post-tool guard armed for scoped native response deltas", async () => {
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
let handleRequest:
| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)
| undefined;
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
return threadStartResult("thread-1");
}
if (method === "turn/start") {
return turnStartResult("turn-1", "inProgress");
}
return {};
});
setCodexAppServerClientFactoryForTest(
async () =>
({
request,
addNotificationHandler: (handler: typeof notify) => {
notify = handler;
return () => undefined;
},
addRequestHandler: (
handler: (request: {
id: string;
method: string;
params?: unknown;
}) => Promise<unknown>,
) => {
handleRequest = handler;
return () => undefined;
},
}) as never,
);
const params = createParams(
path.join(tempDir, "session-scoped-delta-timeout.jsonl"),
path.join(tempDir, "workspace-scoped-delta-timeout"),
);
params.timeoutMs = 2_000;
const run = runCodexAppServerAttempt(params, {
turnCompletionIdleTimeoutMs: 500,
turnAssistantCompletionIdleTimeoutMs: 5,
postToolRawAssistantCompletionIdleTimeoutMs: 50,
turnTerminalIdleTimeoutMs: 500,
});
await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function"), fastWait);
await handleRequest?.({
id: "request-tool-1",
method: "item/tool/call",
params: {
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "message",
arguments: { action: "send", text: "already sent" },
},
});
await notify({
method: "rawResponseItem/completed",
params: {
threadId: "thread-1",
turnId: "turn-1",
item: {
type: "message",
id: "raw-status-1",
role: "assistant",
content: [{ type: "output_text", text: "I'm writing a large patch now." }],
},
},
});
await new Promise((resolve) => setTimeout(resolve, 30));
await notify({
method: "response.custom_tool_call_input.delta",
params: {
threadId: "thread-1",
turnId: "turn-1",
item_id: "ctc-large-edit-1",
output_index: 0,
delta: '{"cmd":"apply_patch","patch":"large chunk"}',
},
});
const result = await run;
expect(result.timedOut).toBe(true);
expect(result.promptError).toBe(
"codex app-server turn idle timed out waiting for turn/completed",
);
});
it("ignores unscoped native response deltas while another turn leases the client", async () => {
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
let handleRequest:
| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)
| undefined;
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
return threadStartResult("thread-1");
}
if (method === "turn/start") {
return turnStartResult("turn-1", "inProgress");
}
return {};
});
setCodexAppServerClientFactoryForTest(
async () =>
({
request,
getActiveSharedLeaseCountForUnscopedNotifications: () => 2,
addNotificationHandler: (handler: typeof notify) => {
notify = handler;
return () => undefined;
},
addRequestHandler: (
handler: (request: {
id: string;
method: string;
params?: unknown;
}) => Promise<unknown>,
) => {
handleRequest = handler;
return () => undefined;
},
}) as never,
);
const params = createParams(
path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"),
);
params.timeoutMs = 60_000;
let settled = false;
const run = runCodexAppServerAttempt(params, {
turnCompletionIdleTimeoutMs: 500,
turnAssistantCompletionIdleTimeoutMs: 5,
postToolRawAssistantCompletionIdleTimeoutMs: 80,
turnTerminalIdleTimeoutMs: 500,
}).finally(() => {
settled = true;
});
await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function"), fastWait);
await handleRequest?.({
id: "request-tool-1",
method: "item/tool/call",
params: {
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "message",
arguments: { action: "send", text: "already sent" },
},
});
await notify({
method: "rawResponseItem/completed",
params: {
threadId: "thread-1",
turnId: "turn-1",
item: {
type: "message",
id: "raw-status-1",
role: "assistant",
content: [{ type: "output_text", text: "I'm writing a large patch now." }],
},
},
});
await new Promise((resolve) => setTimeout(resolve, 40));
await notify({
method: "response.custom_tool_call_input.delta",
params: {
item_id: "foreign-large-edit-1",
output_index: 0,
delta: '{"cmd":"apply_patch","patch":"other turn"}',
},
});
await vi.waitFor(() => expect(settled).toBe(true), fastWait);
const result = await run;
expect(result.aborted).toBe(true);
expect(result.timedOut).toBe(true);
expect(result.promptError).toBe(
"codex app-server turn idle timed out waiting for turn/completed",
);
const completionWarnCall = warn.mock.calls.find(
([message]) => message === "codex app-server turn idle timed out waiting for completion",
);
const completionWarnData = completionWarnCall?.[1] as
| {
lastActivityReason?: string;
lastNotificationMethod?: string;
}
| undefined;
expect(completionWarnData?.lastActivityReason).toBe("notification:rawResponseItem/completed");
expect(completionWarnData?.lastNotificationMethod).toBe("rawResponseItem/completed");
});
it("times out post-native-tool raw assistant progress after the post-tool timeout", async () => {
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
@@ -1362,6 +1677,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
const run = runCodexAppServerAttempt(params, {
turnCompletionIdleTimeoutMs: 100,
turnAssistantCompletionIdleTimeoutMs: 5,
postToolRawAssistantCompletionIdleTimeoutMs: 5,
turnTerminalIdleTimeoutMs: 500,
});
await vi.waitFor(
@@ -1831,6 +2147,7 @@ describe("runCodexAppServerAttempt turn watches", () => {
promptError: "codex app-server turn idle timed out waiting for turn/completed",
codexAppServerFailure: {
kind: "turn_completion_idle_timeout",
turnWatchTimeoutKind: "completion",
transport: "stdio",
threadId: "thread-1",
turnId: "turn-1",

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
clearCodexAppServerBinding,
clearCodexAppServerBindingForThread,
readCodexAppServerBinding,
resolveCodexAppServerBindingPath,
writeCodexAppServerBinding,
@@ -302,4 +303,26 @@ describe("codex app-server session binding", () => {
await clearCodexAppServerBinding(sessionFile);
await expect(readCodexAppServerBinding(sessionFile)).resolves.toBeUndefined();
});
it("clears a binding only when the thread matches", async () => {
const sessionFile = path.join(tempDir, "session.json");
await writeCodexAppServerBinding(sessionFile, {
threadId: "thread-current",
cwd: tempDir,
model: "gpt-5.4-codex",
modelProvider: "openai",
});
await expect(
clearCodexAppServerBindingForThread(sessionFile, "thread-transient"),
).resolves.toBe(false);
await expect(readCodexAppServerBinding(sessionFile)).resolves.toMatchObject({
threadId: "thread-current",
});
await expect(clearCodexAppServerBindingForThread(sessionFile, "thread-current")).resolves.toBe(
true,
);
await expect(readCodexAppServerBinding(sessionFile)).resolves.toBeUndefined();
});
});

View File

@@ -300,6 +300,27 @@ export async function clearCodexAppServerBinding(
}
}
export async function clearCodexAppServerBindingForThread(
sessionFile: string,
threadId: string,
lookup: Omit<CodexAppServerAuthProfileLookup, "authProfileId"> = {},
): Promise<boolean> {
const binding = await readCodexAppServerBinding(sessionFile, lookup);
if (!binding) {
return false;
}
if (binding.threadId !== threadId) {
embeddedAgentLog.debug("codex app-server binding points at a different thread; preserving", {
sessionFile,
threadId,
boundThreadId: binding.threadId,
});
return false;
}
await clearCodexAppServerBinding(sessionFile);
return true;
}
function isNotFound(error: unknown): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
}

View File

@@ -96,7 +96,7 @@ function readLegacySharedCodexAppServerClientState(
return value as LegacySharedCodexAppServerClientState;
}
type SharedCodexAppServerClientOptions = {
type CodexAppServerClientOptions = {
startOptions?: CodexAppServerStartOptions;
timeoutMs?: number;
authProfileId?: string | null;
@@ -104,14 +104,47 @@ type SharedCodexAppServerClientOptions = {
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
};
type ResolvedCodexAppServerClientStartContext = {
agentDir: string;
usesNativeAuth: boolean;
authProfileId: string | undefined;
startOptions: CodexAppServerStartOptions;
};
async function resolveCodexAppServerClientStartContext(
options?: CodexAppServerClientOptions,
): Promise<ResolvedCodexAppServerClientStartContext> {
const agentDir = options?.agentDir ?? resolveDefaultAgentDir(options?.config ?? {});
const usesNativeAuth = options?.authProfileId === null;
const requestedAuthProfileId =
options?.authProfileId === null ? undefined : options?.authProfileId;
const authProfileId = usesNativeAuth
? undefined
: resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: requestedAuthProfileId,
agentDir,
config: options?.config,
});
const requestedStartOptions =
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
const startOptions = await bridgeCodexAppServerStartOptions({
startOptions: managedStartOptions,
agentDir,
authProfileId: usesNativeAuth ? null : authProfileId,
config: options?.config,
});
return { agentDir, usesNativeAuth, authProfileId, startOptions };
}
export async function getSharedCodexAppServerClient(
options?: SharedCodexAppServerClientOptions,
options?: CodexAppServerClientOptions,
): Promise<CodexAppServerClient> {
return (await acquireSharedCodexAppServerClient(options)).client;
}
export async function getLeasedSharedCodexAppServerClient(
options?: SharedCodexAppServerClientOptions,
options?: CodexAppServerClientOptions,
): Promise<CodexAppServerClient> {
const acquired = await acquireSharedCodexAppServerClient(options, { leased: true });
const state = getSharedCodexAppServerClientState();
@@ -139,36 +172,18 @@ export function releaseLeasedSharedCodexAppServerClient(client: CodexAppServerCl
}
async function acquireSharedCodexAppServerClient(
options?: SharedCodexAppServerClientOptions,
options?: CodexAppServerClientOptions,
): Promise<{ client: CodexAppServerClient }>;
async function acquireSharedCodexAppServerClient(
options: SharedCodexAppServerClientOptions | undefined,
options: CodexAppServerClientOptions | undefined,
leaseOptions: { leased: true },
): Promise<{ client: CodexAppServerClient; release: () => void }>;
async function acquireSharedCodexAppServerClient(
options?: SharedCodexAppServerClientOptions,
options?: CodexAppServerClientOptions,
leaseOptions?: { leased: true },
): Promise<{ client: CodexAppServerClient; release?: () => void }> {
const agentDir = options?.agentDir ?? resolveDefaultAgentDir(options?.config ?? {});
const usesNativeAuth = options?.authProfileId === null;
const requestedAuthProfileId =
options?.authProfileId === null ? undefined : options?.authProfileId;
const authProfileId = usesNativeAuth
? undefined
: resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: requestedAuthProfileId,
agentDir,
config: options?.config,
});
const requestedStartOptions =
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
const startOptions = await bridgeCodexAppServerStartOptions({
startOptions: managedStartOptions,
agentDir,
authProfileId: usesNativeAuth ? null : authProfileId,
config: options?.config,
});
const { agentDir, usesNativeAuth, authProfileId, startOptions } =
await resolveCodexAppServerClientStartContext(options);
const fallbackApiKeyCacheKey = authProfileId
? undefined
: resolveCodexAppServerFallbackApiKeyCacheKey({ startOptions });
@@ -184,6 +199,7 @@ async function acquireSharedCodexAppServerClient(
(entry.promise = (async () => {
const client = CodexAppServerClient.start(startOptions);
entry.client = client;
client.setActiveSharedLeaseCountProviderForUnscopedNotifications(() => entry.activeLeases);
client.addCloseHandler((closedClient) => clearSharedClientEntryIfCurrent(key, closedClient));
try {
await client.initialize();
@@ -208,6 +224,7 @@ async function acquireSharedCodexAppServerClient(
options?.timeoutMs ?? 0,
"codex app-server initialize timed out",
);
client.setActiveSharedLeaseCountProviderForUnscopedNotifications(() => entry.activeLeases);
const release = leaseOptions?.leased ? retainSharedClientEntry(entry) : undefined;
return release ? { client, release } : { client };
} catch (error) {
@@ -219,33 +236,11 @@ async function acquireSharedCodexAppServerClient(
}
}
export async function createIsolatedCodexAppServerClient(options?: {
startOptions?: CodexAppServerStartOptions;
timeoutMs?: number;
authProfileId?: string | null;
agentDir?: string;
config?: Parameters<typeof resolveCodexAppServerAuthProfileIdForAgent>[0]["config"];
}): Promise<CodexAppServerClient> {
const agentDir = options?.agentDir ?? resolveDefaultAgentDir(options?.config ?? {});
const usesNativeAuth = options?.authProfileId === null;
const requestedAuthProfileId =
options?.authProfileId === null ? undefined : options?.authProfileId;
const authProfileId = usesNativeAuth
? undefined
: resolveCodexAppServerAuthProfileIdForAgent({
authProfileId: requestedAuthProfileId,
agentDir,
config: options?.config,
});
const requestedStartOptions =
options?.startOptions ?? resolveCodexAppServerRuntimeOptions().start;
const managedStartOptions = await resolveManagedCodexAppServerStartOptions(requestedStartOptions);
const startOptions = await bridgeCodexAppServerStartOptions({
startOptions: managedStartOptions,
agentDir,
authProfileId: usesNativeAuth ? null : authProfileId,
config: options?.config,
});
export async function createIsolatedCodexAppServerClient(
options?: CodexAppServerClientOptions,
): Promise<CodexAppServerClient> {
const { agentDir, usesNativeAuth, authProfileId, startOptions } =
await resolveCodexAppServerClientStartContext(options);
const client = CodexAppServerClient.start(startOptions);
const initialize = client.initialize();
try {

View File

@@ -358,6 +358,7 @@ export async function mirrorCodexAppServerTranscript(params: {
emitSessionTranscriptUpdate({
sessionFile: params.sessionFile,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
message: update.message,
messageId: update.messageId,
messageSeq: update.messageSeq,

View File

@@ -1,7 +1,14 @@
# GitHub Copilot agent runtime (OpenClaw plugin)
Bundled OpenClaw plugin that registers a `copilot` agent harness backed
by `@github/copilot-sdk` and the GitHub Copilot CLI.
External OpenClaw plugin that registers a `copilot` agent harness backed by `@github/copilot-sdk` and the GitHub Copilot CLI.
## Install
```bash
openclaw plugins install @openclaw/copilot
```
Restart the Gateway after installing or updating the plugin.
The harness claims the canonical subscription `github-copilot` provider and
is opt-in only — selection requires explicit `agentRuntime.id: "copilot"`
@@ -13,3 +20,9 @@ configuration, doctor probes, transcript mirroring, compaction, side
questions, replay, and the supported-surface contract.
See [qa/copilot-capabilities.md](../../qa/copilot-capabilities.md)
for the SDK capability inventory the harness is pinned to.
## Package
- Plugin id: `copilot`
- Package: `@openclaw/copilot`
- Minimum OpenClaw host: `2026.5.28`

View File

@@ -1,12 +1,12 @@
{
"name": "openclaw-copilot-sdk-bootstrap",
"version": "1.0.0",
"name": "@openclaw/copilot",
"version": "2026.5.28",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openclaw-copilot-sdk-bootstrap",
"version": "1.0.0",
"name": "@openclaw/copilot",
"version": "2026.5.28",
"dependencies": {
"@github/copilot-sdk": "1.0.0-beta.4"
}

View File

@@ -1,43 +1,39 @@
{
"name": "@openclaw/copilot",
"version": "2026.5.28",
"description": "OpenClaw GitHub Copilot agent runtime plugin (registers a `github-copilot` AgentHarness backed by @github/copilot-sdk over JSON-RPC to the bundled GitHub Copilot CLI)",
"description": "OpenClaw GitHub Copilot agent runtime plugin (registers a `github-copilot` AgentHarness backed by @github/copilot-sdk over JSON-RPC to the GitHub Copilot CLI)",
"repository": {
"type": "git",
"url": "https://github.com/openclaw/openclaw"
},
"type": "module",
"devDependencies": {
"@github/copilot": "1.0.48",
"@github/copilot-sdk": "1.0.0-beta.4",
"@openclaw/plugin-sdk": "workspace:*"
},
"peerDependencies": {
"dependencies": {
"@github/copilot-sdk": "1.0.0-beta.4"
},
"peerDependenciesMeta": {
"@github/copilot-sdk": {
"optional": true
}
"devDependencies": {
"@github/copilot": "1.0.48",
"@openclaw/plugin-sdk": "workspace:*"
},
"openclaw": {
"extensions": [
"./index.ts"
],
"install": {
"clawhubSpec": "clawhub:@openclaw/copilot",
"npmSpec": "@openclaw/copilot",
"defaultChoice": "npm",
"minHostVersion": ">=2026.5.1-beta.1"
"minHostVersion": ">=2026.5.28"
},
"compat": {
"pluginApi": ">=2026.5.28"
},
"build": {
"openclawVersion": "2026.5.28"
"openclawVersion": "2026.5.28",
"bundledDist": false
},
"release": {
"publishToClawHub": false,
"publishToNpm": false
"publishToClawHub": true,
"publishToNpm": true
}
}
}

View File

@@ -136,7 +136,7 @@ export async function probeCopilotCliVersion(
});
return;
}
// Many version commands (notably the bundled `copilot --version`)
// Many version commands (notably the GitHub Copilot CLI's `copilot --version`)
// print a banner plus an "update available" hint on subsequent
// lines. Surface only the first non-empty line as `version` so the
// doctor UI gets a clean string; keep the full stdout in

View File

@@ -117,7 +117,7 @@ describe("sdk-loader", () => {
}
});
it("throws an actionable error with install instructions when both probes fail", async () => {
it("throws an actionable error with plugin install instructions when both probes fail", async () => {
const primaryImport = vi.fn(async () => {
throw new Error("Cannot find module '@github/copilot-sdk'");
});
@@ -134,7 +134,7 @@ describe("sdk-loader", () => {
}),
).rejects.toMatchObject({
code: "COPILOT_SDK_MISSING",
message: expect.stringContaining(COPILOT_SDK_SPEC),
message: expect.stringContaining("openclaw plugins install @openclaw/copilot"),
});
expect(fallbackImport).not.toHaveBeenCalled();
@@ -160,7 +160,8 @@ describe("sdk-loader", () => {
const message = captured?.message ?? "";
expect(message).toContain("primary boom");
expect(message).toContain(path.join(fallbackDir, "node_modules", "@github", "copilot-sdk"));
expect(message).toContain("pnpm add");
expect(message).toContain(COPILOT_SDK_SPEC);
expect(message).toContain("openclaw plugins install @openclaw/copilot");
});
it("caches successful loads across calls when cache is enabled", async () => {
@@ -217,13 +218,8 @@ describe("sdk-loader", () => {
});
});
describe("contract with core copilot-sdk-install", () => {
// We assert literal values rather than importing core's exports because
// extension test files must stay on public plugin-sdk surfaces. The
// symmetric test in src/commands/copilot-sdk-install.test.ts asserts the
// same literals against core's exports, so any drift on either side fails
// one of the two tests.
it("COPILOT_SDK_FALLBACK_DIR matches the canonical core install fallback path", () => {
describe("sdk dependency constants", () => {
it("COPILOT_SDK_FALLBACK_DIR keeps the legacy fallback path stable", () => {
expect(COPILOT_SDK_FALLBACK_DIR).toMatch(/\.openclaw[\\/]+npm-runtime[\\/]+copilot$/);
});
it("COPILOT_SDK_SPEC pins the canonical SDK spec", () => {

View File

@@ -85,14 +85,17 @@ function createMissingSdkError(
const lines = [
"[copilot] @github/copilot-sdk is not installed.",
"",
"The Copilot agent runtime requires @github/copilot-sdk (~260 MB",
"after pulling its platform-specific @github/copilot CLI binary).",
"Install it once with:",
"The external @openclaw/copilot plugin depends on @github/copilot-sdk",
"(~260 MB after pulling its platform-specific @github/copilot CLI binary).",
"Reinstall the plugin once with:",
"",
` pnpm add ${COPILOT_SDK_SPEC}`,
` # or: npm install ${COPILOT_SDK_SPEC}`,
" openclaw plugins install @openclaw/copilot",
"",
`Alternatively, install into the on-demand fallback location at\n ${fallbackPath}`,
"For source checkouts or offline repair, install the SDK directly:",
"",
` npm install ${COPILOT_SDK_SPEC}`,
"",
`The legacy fallback location is still probed at\n ${fallbackPath}`,
"",
"Primary resolution error:",
` ${summarizeError(primaryErr)}`,

View File

@@ -0,0 +1,27 @@
# @openclaw/diagnostics-otel
Official OpenTelemetry diagnostics exporter for OpenClaw.
This plugin exports OpenClaw Gateway traces, metrics, and logs to an OTLP collector for observability stacks such as Grafana, Datadog, Honeycomb, New Relic, Tempo, and compatible collectors.
## Install
```bash
openclaw plugins install @openclaw/diagnostics-otel
```
Restart the Gateway after installing or updating the plugin.
## Configure
Enable the plugin and set the OTLP endpoint in `plugins.entries.diagnostics-otel.config`.
The full config surface, metric names, span names, and collector examples live in the docs:
- https://docs.openclaw.ai/gateway/opentelemetry
## Package
- Plugin id: `diagnostics-otel`
- Package: `@openclaw/diagnostics-otel`
- Minimum OpenClaw host: `2026.4.25`

View File

@@ -0,0 +1,27 @@
# @openclaw/diagnostics-prometheus
Official Prometheus diagnostics exporter for OpenClaw.
This plugin exposes OpenClaw Gateway runtime metrics in Prometheus text format for Prometheus, Grafana, VictoriaMetrics, and compatible scrapers.
## Install
```bash
openclaw plugins install @openclaw/diagnostics-prometheus
```
Restart the Gateway after installing or updating the plugin.
## Configure
Enable the plugin and set the scrape endpoint options in `plugins.entries.diagnostics-prometheus.config`.
The full config surface, metric names, and scrape examples live in the docs:
- https://docs.openclaw.ai/gateway/prometheus
## Package
- Plugin id: `diagnostics-prometheus`
- Package: `@openclaw/diagnostics-prometheus`
- Minimum OpenClaw host: `2026.4.25`

Some files were not shown because too many files have changed in this diff Show More