Compare commits

...

518 Commits

Author SHA1 Message Date
Shadow
27a7b5a60b Changelog: note Discord thread-create content fix 2026-02-16 13:41:36 -06:00
zerone0x
857424bbae fix(discord): send initial message for non-forum thread creation
When creating a thread in a non-forum channel with the `content` parameter,
the content was silently ignored. This happened because only forum/media
channels support the `message` field in the thread creation request body.

This fix sends the initial message as a separate API call after the thread
is created, ensuring the content parameter works for all thread types.

Fixes the issue where `thread-create` with `message` parameter appeared
to succeed but the message never appeared in the thread.
2026-02-16 15:23:51 +01:00
pierreeurope
fec4be8dec fix(cron): prevent daily jobs from skipping days (48h jump) #17852 (#17903)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 1ffe6a45af
Co-authored-by: pierreeurope <248892285+pierreeurope@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:35:49 -05:00
brandonwise
095d522099 fix(security): create session transcript files with 0o600 permissions (#18066)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 962f497d24
Co-authored-by: brandonwise <21148772+brandonwise@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:33:40 -05:00
sebslight
6931f0fb50 refactor(telegram): avoid double-wrapping proxy fetch 2026-02-16 08:24:55 -05:00
sebslight
b4fa10ae67 refactor(infra): make fetch wrapping idempotent 2026-02-16 08:24:55 -05:00
sebslight
7b8cce0910 test(config): normalize merge-patch regression fixture formatting 2026-02-16 08:24:55 -05:00
sebslight
5b8bfd261b test(gateway): cover mixed-id config.patch rollback 2026-02-16 08:24:55 -05:00
sebslight
f4b2fd00bc fix(config): harden object-array merge-by-id fallback 2026-02-16 08:24:55 -05:00
Hongwei Ma
dddb1bc942 fix(telegram): fix streaming with extended thinking models overwriting previous messages/ also happens to Execution error (#17973)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 34b52eead8
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 18:54:34 +05:30
sebslight
553d17f8af refactor(agents): use silent token constant in prompts 2026-02-16 08:20:24 -05:00
Jackten
e3e8046a93 fix(infra): avoid detached finally unhandled rejection in fetch wrapper (#18014)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 4ec21c89cb
Co-authored-by: Jackten <2895479+Jackten@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:17:23 -05:00
不做了睡大觉
cb391f4bdc fix(config): prevent config.patch from destroying arrays when patch entries lack id (#18030)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: a857df9e32
Co-authored-by: stakeswky <64798754+stakeswky@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:13:51 -05:00
sebslight
3a277e394e test(agents): add cooldown expiry helper regressions 2026-02-16 08:10:52 -05:00
sebslight
d224776ffb refactor(agents): extract cooldown probe decision helper 2026-02-16 08:10:52 -05:00
zerone0x
c2a0cf0c28 fix(tts): update tool description to prevent duplicate audio delivery (#18046)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 70c096abaa
Co-authored-by: zerone0x <39543393+zerone0x@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:09:02 -05:00
Ítalo Souza
39bb1b3322 fix: auto-recover primary model after rate-limit cooldown expires (#17478) (#18045)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: f7a7865727
Co-authored-by: PlayerGhost <28265945+PlayerGhost@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-16 08:03:35 -05:00
yinghaosang
244ed9db39 fix(telegram): draft stream preview not threaded when replyToMode is on (#17880) (#17928)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: cfd4181a23
Co-authored-by: yinghaosang <261132136+yinghaosang@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 18:10:24 +05:30
Ayaan Zaidi
b2aa6e094d fix(telegram): prevent non-abort slash commands from racing chat replies (#17899)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5c2f6f2c96
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 16:21:10 +05:30
Advait Paliwal
bc67af6ad8 cron: separate webhook POST delivery from announce (#17901)
* cron: split webhook delivery from announce mode

* cron: validate webhook delivery target

* cron: remove legacy webhook fallback config

* fix: finalize cron webhook delivery prep (#17901) (thanks @advaitpaliwal)

---------

Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
2026-02-16 02:36:00 -08:00
Peter Steinberger
d841c9b26b test: remove duplicate replyToTag assertion in split-tag case 2026-02-16 10:02:59 +00:00
Peter Steinberger
597f956a4f test: remove duplicate existing-id all-mode planner case 2026-02-16 10:01:58 +00:00
Peter Steinberger
f043f2d8c9 test: trim duplicate first-mode hasReplied assertion variant 2026-02-16 10:00:57 +00:00
Peter Steinberger
a4e7f256db test: drop redundant off-mode hasReplied assertion 2026-02-16 09:59:59 +00:00
Peter Steinberger
893f56b87d test: remove redundant multi-variable template resolution case 2026-02-16 09:59:09 +00:00
Peter Steinberger
4da68afc73 test: remove duplicate off-mode existing-id planner case 2026-02-16 09:58:05 +00:00
Peter Steinberger
7cfd0aed5f test: remove duplicate non-date negative-case assertion 2026-02-16 09:56:46 +00:00
Peter Steinberger
d611db8049 test: remove duplicate provider-prefix assertion variant 2026-02-16 09:55:44 +00:00
Peter Steinberger
3eb9c2105c test: remove duplicate date-suffix assertion variant 2026-02-16 09:54:56 +00:00
Peter Steinberger
9f6462bd56 test: trim duplicate latest-suffix assertion variant 2026-02-16 09:54:05 +00:00
Peter Steinberger
2d03473072 test: trim duplicate provider-prefix assertion in short-model tests 2026-02-16 09:52:16 +00:00
Peter Steinberger
dbcdcc5d19 test: remove duplicate positive template-variable detection case 2026-02-16 09:51:09 +00:00
Peter Steinberger
c4297a8d60 test: remove redundant no-provider short-model case 2026-02-16 09:49:58 +00:00
Peter Steinberger
deef9f91bf test: remove duplicate multi-variable template check case 2026-02-16 09:48:51 +00:00
Peter Steinberger
523193a91f test: remove duplicate static template-variable false case 2026-02-16 09:47:45 +00:00
Peter Steinberger
cd04385f9f test: remove redundant provider-plus-date model-name case 2026-02-16 09:46:44 +00:00
Peter Steinberger
82fa526bb0 test: remove duplicate undefined template-variable guard case 2026-02-16 09:45:51 +00:00
Peter Steinberger
3fb4a7eb53 test: remove duplicate hook-wake heartbeat empty-file case 2026-02-16 09:44:16 +00:00
Peter Steinberger
7a6928712b test: remove redundant explicit telegram heartbeat target case 2026-02-16 09:43:01 +00:00
Peter Steinberger
9b351fcbd8 test: remove duplicate whatsapp group heartbeat target case 2026-02-16 09:41:50 +00:00
Peter Steinberger
d3ddf893c2 test: remove redundant store-rotation integration prune case 2026-02-16 09:39:48 +00:00
Peter Steinberger
a597bd26d4 test: remove duplicate direct-enabled whatsapp ack variant 2026-02-16 09:37:42 +00:00
Peter Steinberger
6fa150a890 test: trim redundant whatsapp mention-true ack reaction case 2026-02-16 09:36:02 +00:00
Peter Steinberger
93ad783c1b test: remove redundant slash channel-policy integration case 2026-02-16 09:34:35 +00:00
Peter Steinberger
acc6b62289 test: remove low-value private-channel lookup slash edge case 2026-02-16 09:32:38 +00:00
Peter Steinberger
fec1566f04 test: remove duplicate ack-reaction none-scope branch case 2026-02-16 09:30:33 +00:00
Peter Steinberger
ced5148afd test: remove redundant identity emoji response-prefix case 2026-02-16 09:29:41 +00:00
Peter Steinberger
c0973f24c6 test: remove low-value concurrency passthrough unit case 2026-02-16 09:28:20 +00:00
Peter Steinberger
6a392b8493 test: trim redundant ack-reaction removeAfterReply guard case 2026-02-16 09:27:12 +00:00
Peter Steinberger
f8ae538985 test: remove low-value slash arg-menu payload-shape case 2026-02-16 09:25:34 +00:00
Peter Steinberger
b63f9b7066 test: remove redundant configured memory-flush prompt case 2026-02-16 09:23:03 +00:00
Peter Steinberger
4e3429ae6e test: remove low-value typing-without-consumer variant 2026-02-16 09:22:17 +00:00
Peter Steinberger
78976d3f6f test: remove low-value dm-fallback slash access edge case 2026-02-16 09:21:39 +00:00
Peter Steinberger
e30900f93e test: remove low-value deprecated pruneDays e2e mapping case 2026-02-16 09:20:40 +00:00
Peter Steinberger
cef02df9d5 test: remove redundant explicit-deny slash policy case 2026-02-16 09:19:55 +00:00
Peter Steinberger
0f7ad51020 test: remove low-signal malformed slash button edge case 2026-02-16 09:18:36 +00:00
Peter Steinberger
4e16893c61 test: remove low-value memory-flush ro workspace case 2026-02-16 09:17:47 +00:00
Peter Steinberger
192dbc3ba9 test: drop duplicate role-ordering exception rewrite case 2026-02-16 09:16:11 +00:00
Peter Steinberger
d0b0ca9fcf test: remove low-value open-policy slash channel case 2026-02-16 09:15:18 +00:00
Peter Steinberger
22c53af604 test: remove redundant saveSessionStore cap e2e case 2026-02-16 09:13:56 +00:00
Peter Steinberger
54948a1d44 test: remove redundant maintenance config mapping e2e case 2026-02-16 09:13:05 +00:00
Peter Steinberger
22a1a56e7e test: remove low-value maintenance defaults e2e assertion 2026-02-16 09:11:17 +00:00
Peter Steinberger
15f8c57797 test: speed up subagent announce e2e and drop duplicate defer case 2026-02-16 09:10:11 +00:00
Peter Steinberger
404a8bc35f test: remove redundant pruning-plus-capping e2e case 2026-02-16 09:07:24 +00:00
Peter Steinberger
7a4c131d6b test: remove low-value mirrored-text media-filename unit case 2026-02-16 09:05:38 +00:00
Peter Steinberger
b156aafab9 test: remove low-value direct metadata-mapping unit case 2026-02-16 09:04:20 +00:00
Peter Steinberger
838d875fcb test: remove low-value custom-root agent-extraction path case 2026-02-16 09:03:07 +00:00
Peter Steinberger
7932387df2 test: remove low-value stale-prune no-updatedAt edge case 2026-02-16 09:02:08 +00:00
Peter Steinberger
4d2ba58da5 test: remove low-value legacy dm-direct fallback permutation 2026-02-16 09:00:54 +00:00
Peter Steinberger
7d26eae3ee test: remove low-value no-updatedAt cap-priority edge case 2026-02-16 09:00:02 +00:00
Peter Steinberger
5dc02aa55e test: remove low-value concurrent store-entry merge permutation 2026-02-16 08:58:43 +00:00
Peter Steinberger
c8704297b2 test: remove low-value relative traversal session-file guard case 2026-02-16 08:57:45 +00:00
Peter Steinberger
eb7b5c02c3 test: remove low-value cross-storepath lock parallelism case 2026-02-16 08:56:28 +00:00
Peter Steinberger
314f193030 fix(ci): run scope detection on blacksmith runners 2026-02-16 09:56:11 +01:00
Peter Steinberger
d5bc5ab7ba test: remove low-value resolveStorePath tilde-expansion unit case 2026-02-16 08:54:55 +00:00
Peter Steinberger
fecd623431 test: remove duplicate reset precedence permutation case 2026-02-16 08:53:51 +00:00
Peter Steinberger
1e4cf489e0 fix(ci): keep main runs alive while coalescing newer pushes 2026-02-16 09:53:36 +01:00
Peter Steinberger
5d8f43ae8e test: remove duplicate explicit-agent fallback path case 2026-02-16 08:52:55 +00:00
Peter Steinberger
896f9efcb7 test: remove low-value absolute-in-dir session-file happy path 2026-02-16 08:51:41 +00:00
Peter Steinberger
f448e4bf77 test: remove low-value lock queue cleanup bookkeeping case 2026-02-16 08:50:59 +00:00
Peter Steinberger
ada7a6289f fix(ci): dedupe docker release runs by ref 2026-02-16 09:50:37 +01:00
Peter Steinberger
731d72e119 test: remove redundant in-dir relative session-file acceptance case 2026-02-16 08:49:41 +00:00
Peter Steinberger
bf801f5159 test: remove low-value unknown-session mirror guard case 2026-02-16 08:48:23 +00:00
Peter Steinberger
929a96c2f8 test: remove low-signal mirrored-text trim unit case 2026-02-16 08:47:45 +00:00
Peter Steinberger
2983ef0243 fix(ci): use ref-based concurrency across workflows 2026-02-16 09:47:07 +01:00
Peter Steinberger
b5183c93d6 test: remove low-value lock-storePath guard wrapper test 2026-02-16 08:46:49 +00:00
Peter Steinberger
bd0e7d3d22 test: remove low-value positive session-id validation case 2026-02-16 08:45:30 +00:00
Peter Steinberger
19dfdfe5a8 test: remove low-value missing-session-key mirror guard case 2026-02-16 08:44:46 +00:00
Peter Steinberger
2d6b605cc3 test: remove low-value session-file options wrapper assertion 2026-02-16 08:44:01 +00:00
Peter Steinberger
025d4152d1 fix(ci): key concurrency by ref instead of sha 2026-02-16 09:42:58 +01:00
Peter Steinberger
f9419e26bb test: remove duplicate empty-text mirror integration case 2026-02-16 08:42:38 +00:00
Peter Steinberger
a4f86dc433 test: remove low-value session-file options agent-only case 2026-02-16 08:41:46 +00:00
Peter Steinberger
0c035c85ab test: remove redundant single-error lock queue recovery case 2026-02-16 08:40:34 +00:00
Peter Steinberger
aabc09bb9b test: remove duplicate lock-queue cleanup success case 2026-02-16 08:39:43 +00:00
Peter Steinberger
0d2e13fb73 test: remove redundant transcript-path wrapper case 2026-02-16 08:38:18 +00:00
Peter Steinberger
4f05d045b9 test: remove duplicate absolute outside-session-path guard case 2026-02-16 08:37:19 +00:00
Peter Steinberger
3daaa19426 fix(ci): use JDK 17 for Android SDK setup 2026-02-16 09:36:54 +01:00
Peter Steinberger
ec00efb38d test: remove duplicate reset-by-type direct selection case 2026-02-16 08:36:30 +00:00
Peter Steinberger
83a5f7ba8c test: remove duplicate passthrough storePath guard case 2026-02-16 08:35:14 +00:00
Peter Steinberger
6a759c9191 test: remove duplicate empty-storePath guard case 2026-02-16 08:34:22 +00:00
Peter Steinberger
f6b7736744 test: remove redundant absolute topic-suffix session-file case 2026-02-16 08:33:33 +00:00
Ayaan Zaidi
b6a9741ba4 refactor(telegram): simplify send/dispatch/target handling (#17819)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: fcb7aeeca3
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 14:00:34 +05:30
Peter Steinberger
1f607bec49 test: remove low-value no-rotation file-size case 2026-02-16 08:24:46 +00:00
Peter Steinberger
3dbb69da05 test: remove duplicate session file options fallback case 2026-02-16 08:23:52 +00:00
Peter Steinberger
49d383ba7c test: remove redundant default-root explicit fallback case 2026-02-16 08:23:09 +00:00
Peter Steinberger
e72e8ebe62 test: remove redundant default-root extracted fallback case 2026-02-16 08:22:23 +00:00
Peter Steinberger
374ad8c813 test: remove redundant all-stale pruning case 2026-02-16 08:20:51 +00:00
Peter Steinberger
6f4da72cb5 test: remove redundant cap-entry default-fallback case 2026-02-16 08:19:06 +00:00
Peter Steinberger
facf53cc3f test: remove redundant stale-prune default-fallback case 2026-02-16 08:17:51 +00:00
Peter Steinberger
eaec65656f test: remove redundant cap-entry under-limit case 2026-02-16 08:16:34 +00:00
Peter Steinberger
dfaca933c6 test: remove redundant rotate timestamp case 2026-02-16 08:15:20 +00:00
Peter Steinberger
aaf308d7ec test: remove redundant stale-prune under-limit case 2026-02-16 08:13:57 +00:00
Peter Steinberger
d115d48a72 test: remove redundant rotate missing-file no-op case 2026-02-16 08:12:38 +00:00
Peter Steinberger
d174c38737 test: remove redundant stale-prune boundary case 2026-02-16 08:11:40 +00:00
Peter Steinberger
005dbdd13e test: remove redundant stale-prune empty-store case 2026-02-16 08:09:56 +00:00
Peter Steinberger
c31e33cd18 test: remove redundant stale-prune return-count case 2026-02-16 08:08:59 +00:00
Peter Steinberger
f4fbfae97e test: remove redundant cap-entry empty-store case 2026-02-16 08:07:35 +00:00
Peter Steinberger
f349d40e62 test: remove redundant cap-entry return-count case 2026-02-16 08:06:41 +00:00
Peter Steinberger
d71779b46f test: remove redundant session-rotation exact-limit case 2026-02-16 08:05:26 +00:00
Peter Steinberger
df062fdb63 test: remove redundant cap-entry exact-limit case 2026-02-16 08:04:24 +00:00
Peter Steinberger
52ddaed795 test: remove redundant elide exact-limit case 2026-02-16 08:03:02 +00:00
Peter Steinberger
b7a20d8e8d test: remove redundant depth-1 subagent session case 2026-02-16 08:02:10 +00:00
Peter Steinberger
5cb228fdd0 test: remove redundant quick-reply truncation case 2026-02-16 08:00:47 +00:00
Peter Steinberger
8fd6d4d6dd test: remove redundant messageAction default-text case 2026-02-16 07:59:19 +00:00
Peter Steinberger
242e8f5c43 test: remove low-signal line account listing coverage 2026-02-16 07:58:00 +00:00
Peter Steinberger
4aab640fd1 test: remove redundant default-account normalization case 2026-02-16 07:56:41 +00:00
Peter Steinberger
35b6ccd62c test: remove redundant rich-menu action passthrough case 2026-02-16 07:55:39 +00:00
Peter Steinberger
7e1f542233 test: remove redundant uriAction passthrough case 2026-02-16 07:54:37 +00:00
Peter Steinberger
5927c53630 test: remove redundant postback displayText passthrough case 2026-02-16 07:53:10 +00:00
Peter Steinberger
8505577218 test: remove redundant line account-id env listing case 2026-02-16 07:52:08 +00:00
Peter Steinberger
b18b85dc77 test: remove redundant default rich-menu command smoke case 2026-02-16 07:51:04 +00:00
Peter Steinberger
f3eb003db9 test: remove redundant quick-reply creation smoke case 2026-02-16 07:50:02 +00:00
Peter Steinberger
0448693f8f test: remove redundant messageAction passthrough case 2026-02-16 07:49:15 +00:00
Peter Steinberger
e86647889c test: remove redundant datetimepicker passthrough case 2026-02-16 07:47:36 +00:00
Peter Steinberger
993a5e63a1 test: remove redundant yes-no label passthrough case 2026-02-16 07:46:48 +00:00
Peter Steinberger
c01e97f124 test: remove redundant list-card action passthrough case 2026-02-16 07:45:09 +00:00
Peter Steinberger
bf2d78505e test: remove redundant notification title passthrough case 2026-02-16 07:43:03 +00:00
Peter Steinberger
91337b4b6f test: remove redundant confirm alt-text passthrough case 2026-02-16 07:42:09 +00:00
Peter Steinberger
f74d56bd3b test: remove redundant image-card aspect ratio passthrough case 2026-02-16 07:40:45 +00:00
Peter Steinberger
56d0ad6942 test: remove redundant action-card hero passthrough case 2026-02-16 07:39:50 +00:00
Peter Steinberger
5997a4b0ef test: remove redundant media-player image passthrough case 2026-02-16 07:39:01 +00:00
Peter Steinberger
720aa3c1e6 test: drop redundant line info-card default footer case 2026-02-16 07:37:41 +00:00
Peter Steinberger
223e2a7127 test: remove redundant button thumbnail passthrough case 2026-02-16 07:36:45 +00:00
Peter Steinberger
31ab8ad46d test: remove overlapping short line grid layout case 2026-02-16 07:35:16 +00:00
Peter Steinberger
82a8fc0bc7 test: remove redundant yes-no default template case 2026-02-16 07:34:18 +00:00
Peter Steinberger
227e31d791 test: remove redundant line default menu config case 2026-02-16 07:32:30 +00:00
Peter Steinberger
357b1e8fee test: remove duplicate line account listing case 2026-02-16 07:30:37 +00:00
Peter Steinberger
4c46c23ca8 test: remove redundant default line account id case 2026-02-16 07:29:10 +00:00
Peter Steinberger
189b2e0588 test: remove redundant line default-menu bounds case 2026-02-16 07:28:02 +00:00
Peter Steinberger
a39c2263e5 test: prune overlapping line markdown conversion cases 2026-02-16 07:26:43 +00:00
Peter Steinberger
0490d0e173 test: drop redundant product carousel limit case 2026-02-16 07:25:16 +00:00
Peter Steinberger
64a0339d58 test: trim redundant line quick-reply account checks 2026-02-16 07:23:40 +00:00
Peter Steinberger
077130bdb8 test: remove overlapping line webhook/account cases 2026-02-16 07:22:30 +00:00
Peter Steinberger
12d6b3b0c9 test: prune redundant line action-type checks 2026-02-16 07:20:57 +00:00
Peter Steinberger
3028a1bd3e test: remove redundant line template type assertions 2026-02-16 07:19:41 +00:00
Peter Steinberger
57e055ddb5 test: remove line text quick-reply passthrough tests 2026-02-16 07:17:39 +00:00
Peter Steinberger
4fd008e918 test: remove redundant flex message wrapper test 2026-02-16 07:16:46 +00:00
Peter Steinberger
d39b8541f8 test: prune redundant markdown extractor plain-text negatives 2026-02-16 07:15:47 +00:00
Peter Steinberger
ac4183edd7 test: remove redundant line existence assertions 2026-02-16 07:14:54 +00:00
Peter Steinberger
838963d66c test: drop low-signal line media player footer assertion 2026-02-16 07:13:47 +00:00
Peter Steinberger
4852dd4503 test: remove duplicate line flex wrapper coverage 2026-02-16 07:12:52 +00:00
Peter Steinberger
4d1cb661fc test: remove redundant line link menu wrapper test 2026-02-16 07:11:16 +00:00
Peter Steinberger
3bd961f00a test: drop duplicate line quick-reply wrapper assertion 2026-02-16 07:10:19 +00:00
Peter Steinberger
583345fdfe test: collapse redundant markdown conversion micro-tests 2026-02-16 07:09:31 +00:00
Peter Steinberger
3d550ed4c3 test: remove low-signal line card existence tests 2026-02-16 07:08:32 +00:00
Peter Steinberger
c37cc5ffad test: trim redundant markdown strip and table layout checks 2026-02-16 07:07:07 +00:00
Peter Steinberger
b83ccfba13 test: remove redundant line flex baseline checks 2026-02-16 07:04:56 +00:00
Peter Steinberger
8ea890e8fb test: remove duplicate line quick-reply assertions 2026-02-16 07:03:51 +00:00
Peter Steinberger
ae6060d777 test: remove redundant line markdown conversion smoke checks 2026-02-16 07:02:37 +00:00
Peter Steinberger
ec708b6ab5 test: trim redundant line action helper smoke checks 2026-02-16 07:01:43 +00:00
Peter Steinberger
944a32cf02 test: remove redundant line flex smoke checks 2026-02-16 06:59:46 +00:00
Peter Steinberger
c4880675e1 test: prune redundant line template constructor checks 2026-02-16 06:58:33 +00:00
Peter Steinberger
8b6537d857 test: trim redundant line template shape checks 2026-02-16 06:57:15 +00:00
Peter Steinberger
12c3821acb test: prune low-signal line flex template checks 2026-02-16 06:55:49 +00:00
Peter Steinberger
a69c06e3cc test: remove duplicate daemon profile trim wrappers 2026-02-16 06:53:13 +00:00
Peter Steinberger
67aa7eefe5 test: remove redundant sticker thread id assertion 2026-02-16 06:51:50 +00:00
Peter Steinberger
425c715a05 test: remove duplicate sticker recipient normalization checks 2026-02-16 06:50:44 +00:00
Peter Steinberger
dcba3e5699 test: trim redundant telegram thread+reply combination checks 2026-02-16 06:49:17 +00:00
Peter Steinberger
27083e6f1a test: remove redundant telegram requireMention negative case 2026-02-16 06:47:45 +00:00
Peter Steinberger
18bb242316 test: remove duplicate line action creator coverage 2026-02-16 06:46:21 +00:00
the sun gif man
68ea063958 🤖 fix: preserve openai reasoning replay ids (#17792)
What:
- disable tool-call id sanitization for OpenAI/OpenAI Codex transcript policy
- gate id sanitization in image sanitizer to full mode only
- keep orphan reasoning downgrade scoped to OpenAI model-switch replay path
- update transcript policy, session-history, sanitizer, and reasoning replay tests
- document OpenAI model-switch orphan-reasoning cleanup behavior in transcript hygiene reference

Why:
- OpenAI Responses replay depends on canonical call_id|fc_id pairings for reasoning followers
- strict id rewriting in OpenAI path breaks follower matching and triggers rs_* orphan 400s
- limiting scope avoids behavior expansion while fixing the identified regression

Tests:
- pnpm vitest run src/agents/transcript-policy.test.ts src/agents/pi-embedded-runner.sanitize-session-history.test.ts src/agents/openai-responses.reasoning-replay.test.ts
- pnpm vitest run --config vitest.e2e.config.ts src/agents/transcript-policy.e2e.test.ts src/agents/pi-embedded-runner.sanitize-session-history.e2e.test.ts src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.e2e.test.ts src/agents/pi-embedded-helpers.sanitizeuserfacingtext.e2e.test.ts
- pnpm lint
- pnpm format:check
- pnpm check:docs
- pnpm test (fails in current macOS bash 3.2 env at test/git-hooks-pre-commit.integration.test.ts: mapfile not found)
2026-02-15 22:45:01 -08:00
Peter Steinberger
eefda1314f test: drop duplicate telegram username allowFrom check 2026-02-16 06:44:38 +00:00
Peter Steinberger
a8a22920f1 test: remove duplicate telegram allowFrom cases 2026-02-16 06:43:24 +00:00
Peter Steinberger
a8084b24d6 test: trim additional low-signal flex template checks 2026-02-16 06:40:26 +00:00
Peter Steinberger
97d5ff3500 test: remove low-signal flex template option-only assertions 2026-02-16 06:38:41 +00:00
Peter Steinberger
abb7618b0f test: remove pass-through rich menu action mode checks 2026-02-16 06:37:38 +00:00
Peter Steinberger
1ec0f3b81d test: drop redundant daemon profile normalization wrappers 2026-02-16 06:36:15 +00:00
Peter Steinberger
6c3e7896c5 test: remove duplicate lowercase default profile daemon path cases 2026-02-16 06:34:05 +00:00
Peter Steinberger
2a5fa426f2 test: remove redundant schtasks command parsing cases 2026-02-16 06:32:59 +00:00
Peter Steinberger
29203884c2 test: consolidate gateway profile normalization coverage 2026-02-16 06:31:36 +00:00
Peter Steinberger
91e120870f test: remove duplicate uppercase default profile daemon cases 2026-02-16 06:29:20 +00:00
Peter Steinberger
6a9ead3813 test: remove duplicate profile-specific daemon constant cases 2026-02-16 06:28:15 +00:00
Peter Steinberger
cb998aa7f9 test: remove duplicate systemd exec-start split assertion 2026-02-16 06:27:19 +00:00
Peter Steinberger
ac02e45a88 test: drop redundant empty-profile extraction cases 2026-02-16 06:25:18 +00:00
Peter Steinberger
8f603ec03d test: remove duplicate default-profile casing checks 2026-02-16 06:23:34 +00:00
Peter Steinberger
84e0ee3c31 test: remove duplicate uppercase default profile case 2026-02-16 06:22:04 +00:00
Peter Steinberger
da2bdbef7e test: remove duplicate systemd exec-start split case 2026-02-16 06:21:13 +00:00
Peter Steinberger
1be7c4ba8e test: move session store pruning integration suite to e2e lane 2026-02-16 06:19:49 +00:00
Peter Steinberger
a82df1015b test: remove duplicate hr spacing assertion 2026-02-16 06:16:33 +00:00
Peter Steinberger
a0b459b8f9 test: remove duplicate undefined-profile default cases 2026-02-16 06:15:26 +00:00
Peter Steinberger
28118ca051 test: drop duplicate internal hook lifecycle case 2026-02-16 06:14:23 +00:00
Peter Steinberger
d374a64658 test: move skills-cli integration coverage to e2e lane 2026-02-16 06:13:46 +00:00
Peter Steinberger
0895bb6de6 test: move skills-install fallback suite to e2e lane 2026-02-16 06:11:01 +00:00
Peter Steinberger
189cba0100 test: remove duplicate sandbox access memory-flush case 2026-02-16 06:08:44 +00:00
Peter Steinberger
108ebc380f test: remove duplicate registry throw assertion 2026-02-16 06:06:24 +00:00
Peter Steinberger
93e62d8e3e test: remove duplicate slack dm authorization case 2026-02-16 06:04:58 +00:00
Peter Steinberger
ed28ad2822 test: remove duplicate configured canonical key case 2026-02-16 06:03:09 +00:00
Peter Steinberger
00bbddeef5 test: move git hook regression to e2e lane 2026-02-16 06:01:07 +00:00
Peter Steinberger
5ac59e6e02 test: remove duplicate availability-unavailable fallback case 2026-02-16 05:58:49 +00:00
Peter Steinberger
bfb5a44089 test: speed up plugin optional tools suite 2026-02-16 05:56:26 +00:00
Peter Steinberger
599195fb31 test: trim duplicate antigravity availability case 2026-02-16 05:53:54 +00:00
Peter Steinberger
705d83aec7 test: drop duplicate z-ai alias filter case 2026-02-16 05:41:58 +00:00
Peter Steinberger
c80017e704 test: trim duplicate z.ai provider alias case 2026-02-16 05:39:42 +00:00
Peter Steinberger
9e67f9d889 test: remove duplicate invalid slash-button case 2026-02-16 05:36:55 +00:00
Peter Steinberger
9383f85046 test: trim redundant web media prefix coverage 2026-02-16 05:34:08 +00:00
Peter Steinberger
5212d1c79e test: make sandbox symlink-escape assertion platform-aware 2026-02-16 06:26:08 +01:00
Peter Steinberger
7aa7b04fb0 test: rebalance isolated unit test lane 2026-02-16 05:22:00 +00:00
Peter Steinberger
b3d3f36360 test: speed up slack slash monitor tests 2026-02-16 05:20:22 +00:00
Peter Steinberger
2b6f8548c9 test: trim pre-commit hook integration setup 2026-02-16 05:15:55 +00:00
Peter Steinberger
9684ae4c6d test: tighten process timeout thresholds with stabilized emit guard 2026-02-16 05:09:47 +00:00
Peter Steinberger
39fa81dc96 chore: bump version to 2026.2.16 2026-02-16 06:08:47 +01:00
Peter Steinberger
f1654b4ba2 test: isolate telegram bot behavior suite from unit-fast lane 2026-02-16 04:50:19 +00:00
Peter Steinberger
0b780789bc test: further reduce process timeout waits in fast suites 2026-02-16 04:48:55 +00:00
Peter Steinberger
795874711b test: shorten process timeout waits in exec and supervisor suites 2026-02-16 04:45:44 +00:00
Peter Steinberger
17d8e2a1c8 test: reduce supervisor no-output wait threshold 2026-02-16 04:43:33 +00:00
Peter Steinberger
c53e4e6c8f test: trim exec timeout waits for faster suite runtime 2026-02-16 04:41:45 +00:00
Peter Steinberger
4f5bc0a493 chore(release): align 2026.2.15 metadata 2026-02-16 05:38:58 +01:00
Peter Steinberger
92ec3ddc14 test: drop brittle pre-commit script structure test 2026-02-16 04:35:54 +00:00
Peter Steinberger
510889d439 test: isolate slack slash and telegram bootstrap suites 2026-02-16 04:34:51 +00:00
Peter Steinberger
ceddb4a593 style(memory): format flaky ci test files 2026-02-16 05:32:42 +01:00
Peter Steinberger
794808b169 test: isolate hook installer suite from unit-fast lane 2026-02-16 04:31:30 +00:00
Peter Steinberger
fb6dba2058 fix(ci): align tar override and lockfile to 7.5.9 2026-02-16 05:30:02 +01:00
Varun Kruthiventi
c62b90a2b7 fix(telegram): stop block streaming from splitting messages when streamMode is off (#17704)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 847162caad
Co-authored-by: saivarunk <2976867+saivarunk@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-16 09:57:29 +05:30
Peter Steinberger
1b223dbdd8 test: isolate git-hooks integration and stabilize exec timeout 2026-02-16 04:24:00 +00:00
Peter Steinberger
e7ccbd1445 test: isolate block-streaming suite from unit-fast lane 2026-02-16 04:20:21 +00:00
Peter Steinberger
bc65e787c8 test: trim process no-output timeout waits 2026-02-16 04:17:38 +00:00
Vignesh Natarajan
3e1986f119 chore (changelog): note qmd collection isolation fix 2026-02-15 20:15:12 -08:00
Vignesh Natarajan
b32ae6fa0c fix (memory/qmd): isolate managed collections per agent 2026-02-15 20:14:45 -08:00
Peter Steinberger
5d436f48b2 docs: add mac beta release runbook note 2026-02-16 05:13:49 +01:00
Peter Steinberger
90800cd23e chore(release): update appcast for 2026.2.15 2026-02-16 05:10:59 +01:00
Peter Steinberger
f52805a783 test: reuse heartbeat suite fixtures across cases 2026-02-16 04:10:51 +00:00
Peter Steinberger
a7385aa8ac test: reduce process timeout test latency 2026-02-16 04:08:50 +00:00
Peter Steinberger
e8a50e41a5 test: reuse fixtures in skills install fallback suite 2026-02-16 04:03:24 +00:00
Peter Steinberger
83ce48302f test: trim timeout-heavy exec and telegram cases 2026-02-16 04:00:53 +00:00
Peter Steinberger
25dc4293bf test: speed up isolated-agent and pty test suites 2026-02-16 03:58:43 +00:00
Peter Steinberger
3fe22ea2fd chore(release): align .15 changelog ordering and release notes 2026-02-16 04:50:24 +01:00
Peter Steinberger
31939397a9 test: optimize hot-path test runtime 2026-02-16 03:49:05 +00:00
Ayaan Zaidi
9b2e1769c5 docs(contributing): update maintainers list (#17719)
* docs(contributing): refresh maintainer list

* docs(contributing): fix tyler x handle

* docs(contributing): add discord admin scope for shadow

* docs(contributing): add irc scope for vignesh
2026-02-16 09:18:35 +05:30
Peter Steinberger
61a865031f fix: sync pnpm lockfile for docker onboard 2026-02-16 04:45:42 +01:00
Peter Steinberger
ae6fe67550 test: align e2e coverage with supervisor session flow 2026-02-16 03:41:58 +00:00
Peter Steinberger
702b94fe8f style(line): format files to unblock ci check 2026-02-16 03:39:41 +00:00
Peter Steinberger
b5a63e18f9 test(sandbox): add array-order hash and recreate regression tests 2026-02-16 04:36:24 +01:00
Vignesh Natarajan
78277152ca test(heartbeat): cover telegram showOk suppression 2026-02-15 19:35:25 -08:00
Peter Steinberger
d1fca442b4 refactor(sandbox): centralize sha256 helpers 2026-02-16 04:33:47 +01:00
Sebastian
3c467baa2d test(skills): add status-to-install apt fallback coverage 2026-02-15 22:32:51 -05:00
Sebastian
c8e110e2e3 refactor(skills): extract installer strategy helpers 2026-02-15 22:32:51 -05:00
Peter Steinberger
41ded303b4 fix(sandbox): preserve array order in config hashing 2026-02-16 04:32:03 +01:00
Vignesh Natarajan
cbf58d2e1c fix(memory): harden context window cache collisions 2026-02-15 19:31:52 -08:00
Peter Steinberger
559c8d9930 fix: replace deprecated SHA-1 in sandbox config hash 2026-02-16 04:30:59 +01:00
Peter Steinberger
aef1d55300 fix(cron): normalize skill-filter snapshots and split isolated run helpers 2026-02-16 04:27:12 +01:00
Peter Steinberger
6754a926ee fix(pairing): support legacy telegram allowFrom migration 2026-02-16 03:26:07 +00:00
Vignesh Natarajan
18c6f40d32 chore (changelog): credit LINE webhook fail-closed hardening 2026-02-15 19:25:33 -08:00
Vignesh Natarajan
c7bc7249c3 test (security/line): cover missing webhook auth startup paths 2026-02-15 19:25:33 -08:00
Vignesh Natarajan
beb77229c0 fix (security/line): fail closed when webhook auth is missing 2026-02-15 19:25:33 -08:00
McRolly NWANGWU
d19b746928 feat(skills): add cross-platform install fallback for non-brew environments (#17687)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 3ed4850838
Co-authored-by: mcrolly <60803337+mcrolly@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-15 22:25:26 -05:00
Vignesh Natarajan
9df21da129 chore (changelog): credit memory flush runtime date fix 2026-02-15 19:20:38 -08:00
Vignesh Natarajan
3087657679 test (memory/compaction): cover resolved memory flush prompt semantics 2026-02-15 19:20:38 -08:00
Vignesh Natarajan
ffbcb37342 fix (memory/compaction): inject runtime date-time into memory flush prompt 2026-02-15 19:20:38 -08:00
Shadow
a61c2dc4bd Discord: add component v2 UI tool support (#17419) 2026-02-15 21:19:25 -06:00
Peter Steinberger
b4a9eacd76 chore: format qmd-manager test 2026-02-16 04:18:42 +01:00
Peter Steinberger
ac2ede5bb1 fix(telegram): treat no-op editMessage as success 2026-02-16 04:18:24 +01:00
Vignesh Natarajan
7089885ac4 chore (changelog): credit unicode FTS tokenization fix 2026-02-15 19:17:06 -08:00
Vignesh Natarajan
501e893676 fix (memory/search): support unicode tokens in FTS query builder 2026-02-15 19:17:03 -08:00
Vignesh Natarajan
82631d225c chore (changelog): credit sandbox prompt path guidance fix 2026-02-15 19:16:02 -08:00
Vignesh Natarajan
799049f586 fix (agents/sandbox): clarify container-vs-host workspace paths in prompt 2026-02-15 19:16:02 -08:00
Peter Steinberger
ab1dc89a2d chore(deps): update dependencies 2026-02-16 04:15:03 +01:00
Vignesh Natarajan
0b3d4b8e57 chore (changelog): credit control-ui scope bypass fix 2026-02-15 19:12:10 -08:00
Vignesh Natarajan
eed02a2b57 fix (security/gateway): preserve control-ui scopes in bypass mode 2026-02-15 19:12:06 -08:00
Vignesh Natarajan
a203430aa3 chore (changelog): credit pairing account isolation fix 2026-02-15 19:10:06 -08:00
Vignesh Natarajan
6cf7c02d4a feat (cli): add account selector for pairing commands 2026-02-15 19:10:06 -08:00
Vignesh Natarajan
6957354d48 fix (telegram/whatsapp): use account-scoped pairing allowlists 2026-02-15 19:10:06 -08:00
Vignesh Natarajan
ee10feb80e fix (security/pairing): scope pairing stores by account 2026-02-15 19:10:06 -08:00
Marcus Castro
61c9935264 fix: correct indentation in cron isolated-agent run.ts 2026-02-16 04:09:39 +01:00
Marcus Castro
e5dbfde7e1 test(cron): add empty-skills edge case for skill filter coverage
Addresses Greptile review feedback: locks in behavior when an agent
has skills: [] (explicit empty list), ensuring skillFilter: [] is
forwarded to buildWorkspaceSkillSnapshot to filter out all skills.
2026-02-16 04:09:39 +01:00
Marcus Castro
053affffec fix(cron): pass agent-level skill filter to isolated cron sessions
Isolated cron sessions called buildWorkspaceSkillSnapshot without
the skillFilter parameter, causing all skills to be included even
when an agent had a restricted skills list via agents.list[].skills.

Resolves the filter using resolveAgentSkillsFilter and passes it
through, aligning isolated cron with main session behavior.

Fixes #10804
2026-02-16 04:09:39 +01:00
Peter Steinberger
e1e46dc11b docs: reorder 2026.2.15 changelog entries by impact 2026-02-16 04:06:46 +01:00
Peter Steinberger
7cd288a8a0 docs: add plugin release fast path notes 2026-02-16 04:06:09 +01:00
Peter Steinberger
38ac4b8083 test(pty): stabilize non-windows signal assertion 2026-02-16 03:06:03 +00:00
Vignesh Natarajan
e7a053b4dd chore (changelog): credit qmd session collection rebind fix 2026-02-15 19:03:59 -08:00
Vignesh Natarajan
85430c8495 fix (memory/qmd): rebind drifted managed collection paths 2026-02-15 19:03:55 -08:00
Vignesh Natarajan
8e162d9319 chore (changelog): credit inbound metadata id fix 2026-02-15 19:01:08 -08:00
Vignesh Natarajan
bed8e7abe6 fix (auto-reply): expose inbound message identifiers in trusted metadata 2026-02-15 19:01:08 -08:00
Peter Steinberger
82333add95 test(sessions): cover sandbox session-tools context 2026-02-16 03:00:25 +00:00
Peter Steinberger
7a4a068124 test(sessions): add access and resolution helper coverage 2026-02-16 02:59:30 +00:00
Peter Steinberger
1a03aad246 refactor(sessions): split access and resolution helpers 2026-02-16 03:56:49 +01:00
Peter Steinberger
2f621876f1 test(gateway): cover basePath bootstrap config endpoint 2026-02-16 02:56:23 +00:00
Peter Steinberger
6dfefa1be1 test(ui): cover trailing-slash bootstrap basePath 2026-02-16 02:55:24 +00:00
Peter Steinberger
c876d24d89 test: expand prompt and update hint coverage 2026-02-16 02:54:06 +00:00
Tag
6802b155a8 fix: stop LLM retry loop when browser control service is unavailable (#17673)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 90f47fe132
Co-authored-by: tag-assistant <260167501+tag-assistant@users.noreply.github.com>
Co-authored-by: sebslight <19554889+sebslight@users.noreply.github.com>
Reviewed-by: @sebslight
2026-02-15 21:53:49 -05:00
Peter Steinberger
17a148c8a8 fix: always include long-wait polling guidance in prompt 2026-02-16 03:51:38 +01:00
Peter Steinberger
abd26b6e54 refactor(ui): reuse Control UI bootstrap path constant 2026-02-16 03:50:39 +01:00
Peter Steinberger
8985f23de7 test(gateway): move Control UI http coverage 2026-02-16 03:50:39 +01:00
Peter Steinberger
c6e6023e3a refactor(gateway): share Control UI bootstrap contract and CSP 2026-02-16 03:50:39 +01:00
Peter Steinberger
6e7c1c16e7 test: remove duplicate legacy sessions_spawn e2e file 2026-02-16 03:48:51 +01:00
Peter Steinberger
52e240d10d test(status): add coverage for update summary + timestamps 2026-02-16 02:47:47 +00:00
Peter Steinberger
b6305e9725 test(skills): split installer security coverage 2026-02-16 03:47:28 +01:00
Peter Steinberger
2363e1b085 fix(security): restrict skill download target paths 2026-02-16 03:47:28 +01:00
Peter Steinberger
c6c53437f7 fix(security): scope session tools and webhook secret fallback 2026-02-16 03:47:10 +01:00
Peter Steinberger
fbe6d7c701 ci: include a2ui sources in onboarding docker build 2026-02-16 02:45:00 +00:00
Peter Steinberger
2a53eff856 perf: speed up slack slash handler tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
cd37c52624 perf: speed up slack slash tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
17e5a5015c perf: avoid async cron timer callbacks 2026-02-16 02:45:00 +00:00
Peter Steinberger
8515ae6eea perf: consolidate telegram bot test harness 2026-02-16 02:45:00 +00:00
Peter Steinberger
d1de66b6cf perf: speed up gateway lock tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
7eeba3de85 perf: speed up telegram bot suite setup 2026-02-16 02:45:00 +00:00
Peter Steinberger
38c91c5a13 test: speed up skills-cli integration 2026-02-16 02:45:00 +00:00
Peter Steinberger
88033002ba test: consolidate nodes screen helpers 2026-02-16 02:45:00 +00:00
Peter Steinberger
f835301aed test: consolidate channel helper suites 2026-02-16 02:45:00 +00:00
Peter Steinberger
4d4f693f92 test: consolidate media store header extension coverage 2026-02-16 02:45:00 +00:00
Peter Steinberger
d95be2c384 fix: preserve sandbox allow-all semantics 2026-02-16 02:45:00 +00:00
Peter Steinberger
014d45f7ee test: tighten relay smoke + slack token validation 2026-02-16 02:45:00 +00:00
Peter Steinberger
4d9e310dad test: strengthen ports, tool policy, and note wrapping 2026-02-16 02:45:00 +00:00
Peter Steinberger
f50e1e8015 perf(test): fold gateway discover tests into run-loop 2026-02-16 02:45:00 +00:00
Peter Steinberger
aa1e4962da perf(test): fold doctor legacy migration harness cases 2026-02-16 02:45:00 +00:00
Peter Steinberger
ea07d3fdd8 perf(test): consolidate auth/pty/health mini suites 2026-02-16 02:45:00 +00:00
Peter Steinberger
f142048293 perf(test): fold tool-policy + doctor workspace entrypoints 2026-02-16 02:45:00 +00:00
Peter Steinberger
5fe47e7be6 perf(test): fold ports + terminal note suites 2026-02-16 02:45:00 +00:00
Peter Steinberger
3f44ea244f perf(test): fold health + signal mention tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
6b2f40652f perf(test): consolidate daemon test entrypoints 2026-02-16 02:45:00 +00:00
Peter Steinberger
00e79ac897 perf(test): consolidate pi-embedded helpers e2e suites 2026-02-16 02:45:00 +00:00
Peter Steinberger
efe530acdd perf(test): fold session key utils into routing session key suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
d69496c449 perf(test): fold small config suites into config misc 2026-02-16 02:45:00 +00:00
Peter Steinberger
9386075b7b perf(test): fold node-host runner tests into sanitize env suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
e64dd7b56a perf(test): fold markdown list spacing into nested lists suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
0e4eada580 perf(test): fold telegram update offset store into token suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
65b5dbd6c1 perf(test): fold telegram sent-message cache tests into send suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
e770728cb5 perf(test): fold telegram download tests into fetch suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
cb80901cf9 perf(test): fold cron system event filter into system events suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
3e0076c9ce perf(test): drop redundant index entrypoint tests 2026-02-16 02:45:00 +00:00
Peter Steinberger
02124094bf perf(test): fold acp event mapper tests into client suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
37f030a671 perf(test): fold console prefix tests into logger suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
be3c431011 perf(test): fold gateway config schema tests into config misc 2026-02-16 02:45:00 +00:00
Peter Steinberger
b97c5d6158 perf(test): fold sender identity checks into channel config suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
2b4ebcb570 chore: remove accidental a2ui bundle artifacts 2026-02-16 02:45:00 +00:00
Peter Steinberger
2acc0b0f47 perf(test): fold globals unit tests into logger suite 2026-02-16 02:45:00 +00:00
Peter Steinberger
056070c2bf perf(test): fold cron webhook schema coverage into config misc 2026-02-16 02:44:59 +00:00
Peter Steinberger
04004c5663 perf(test): consolidate models-config provider unit tests 2026-02-16 02:44:59 +00:00
Peter Steinberger
e1350ff976 perf(test): fold imessage rpc client guard into targets suite 2026-02-16 02:44:59 +00:00
Peter Steinberger
8d2029a03d perf(test): fold qr-image tests into web login suite 2026-02-16 02:44:59 +00:00
Peter Steinberger
58ab60c0fc perf(test): fold tls fingerprint normalization into ssrf suite 2026-02-16 02:44:59 +00:00
Peter Steinberger
7c27c2d659 refactor(daemon-cli): share status text styling 2026-02-16 02:42:55 +00:00
Peter Steinberger
c1655982d4 refactor: centralize pre-commit file filtering 2026-02-16 03:42:11 +01:00
Peter Steinberger
91c49dd0ea refactor(status): share registry summary formatting 2026-02-16 02:41:30 +00:00
Peter Steinberger
8eecf97cc5 refactor(cli): share gmail webhook option parsing 2026-02-16 02:39:55 +00:00
Peter Steinberger
46e714058c refactor(subagents): dedupe list row builder 2026-02-16 02:38:00 +00:00
Peter Steinberger
1547bb6a07 refactor(auto-reply): share abort persistence 2026-02-16 02:36:18 +00:00
Peter Steinberger
0c8bb361ca refactor(gateway-tool): share write metadata parsing 2026-02-16 02:36:18 +00:00
seewhy
ddcc7a1a5d fix(discord): dedupe native skill commands by skillName (#17365)
* fix(discord): dedupe native skill commands by skill name

* Changelog: credit Discord skill dedupe

---------

Co-authored-by: yume <yume@yumedeMacBook-Pro.local>
Co-authored-by: Shadow <hi@shadowing.dev>
2026-02-15 20:33:51 -06:00
Peter Steinberger
d9d5b53b42 refactor(logging): share local iso timestamp format 2026-02-16 02:32:59 +00:00
Peter Steinberger
9805ce0097 refactor(memory): reuse cached embedding collector 2026-02-16 02:32:59 +00:00
Peter Steinberger
cf69907015 fix(security): redact Telegram bot tokens in errors 2026-02-16 03:30:53 +01:00
Shakker
09566b1693 fix(discord): preserve channel session keys via channel_id fallbacks (#17622)
* fix(discord): preserve channel session keys via channel_id fallbacks

* docs(changelog): add discord session continuity note

* Tests: cover discord channel_id fallback

---------

Co-authored-by: Shadow <hi@shadowing.dev>
2026-02-15 20:30:17 -06:00
Peter Steinberger
39d5590230 refactor(line): reuse reply chunk deps type 2026-02-16 02:29:07 +00:00
Peter Steinberger
35c5d2be5c refactor(telegram): share group allowFrom resolution 2026-02-16 02:27:01 +00:00
Peter Steinberger
b88f377623 fix: make fast-tool stub type portable 2026-02-16 03:23:45 +01:00
Peter Steinberger
ba84b12535 fix: harden pre-commit hook against option injection 2026-02-16 03:23:45 +01:00
Peter Steinberger
dc9808a674 refactor(gateway): dedupe transcript tail preview 2026-02-16 02:21:39 +00:00
Peter Steinberger
60ad2c2e96 refactor(device-pairing): share token update context 2026-02-16 02:19:53 +00:00
Peter Steinberger
a7cbce1b3d refactor(security): tighten sandbox bind validation 2026-02-16 03:19:50 +01:00
Peter Steinberger
a74251d415 refactor(agents): dedupe fast tool stubs 2026-02-16 02:17:45 +00:00
Peter Steinberger
cbc3de6c97 docs(changelog): fix conflict marker 2026-02-16 03:15:57 +01:00
Peter Steinberger
01b1e350b2 docs: note Control UI XSS fix 2026-02-16 03:15:57 +01:00
Peter Steinberger
3b4096e02e fix(ui): load Control UI bootstrap config via JSON endpoint 2026-02-16 03:15:57 +01:00
Peter Steinberger
adc818db4a fix(gateway): serve Control UI bootstrap config and lock down CSP 2026-02-16 03:15:57 +01:00
Peter Steinberger
568fd337be refactor(web-fetch): dedupe firecrawl fallback 2026-02-16 02:15:02 +00:00
Peter Steinberger
d9ca051a1d refactor(auto-reply): share slash parsing for config/debug 2026-02-16 02:11:12 +00:00
Peter Steinberger
1b6704ef53 docs: update sandbox bind mount guidance 2026-02-16 03:05:16 +01:00
Peter Steinberger
887b209db4 fix(security): harden sandbox docker config validation 2026-02-16 03:04:06 +01:00
Peter Steinberger
d4bdcda324 refactor(nodes-cli): share node.invoke param builder 2026-02-16 02:03:15 +00:00
Peter Steinberger
966957fc66 refactor(nodes-cli): share pending pairing table 2026-02-16 01:58:30 +00:00
Peter Steinberger
555eb3f62c refactor(discord): share member access state 2026-02-16 01:55:40 +00:00
Peter Steinberger
93b9f1ec5f docs(changelog): note prompt path injection hardening 2026-02-16 02:53:40 +01:00
Peter Steinberger
6254e96acf fix(security): harden prompt path sanitization 2026-02-16 02:53:40 +01:00
Peter Steinberger
19f53543d2 refactor(utils): share chunkItems helper 2026-02-16 01:52:30 +00:00
Peter Steinberger
618008b483 refactor(channels): share directory allowFrom parsing 2026-02-16 01:49:16 +00:00
Peter Steinberger
31d1ed351f refactor(channels): dedupe status account bits 2026-02-16 01:47:52 +00:00
Peter Steinberger
22c1210a16 refactor(auto-reply): share directive level resolution 2026-02-16 01:45:51 +00:00
Peter Steinberger
273d70741f refactor(supervisor): share env normalization 2026-02-16 01:41:35 +00:00
Peter Steinberger
07be14c02d refactor(gateway): dedupe chat session abort flow 2026-02-16 01:39:39 +00:00
Peter Steinberger
5b2cb8ba11 refactor(cron): dedupe finished event emit 2026-02-16 01:37:03 +00:00
Peter Steinberger
1d7b2bc9c8 refactor(slack): dedupe slash reply delivery 2026-02-16 01:35:46 +00:00
Peter Steinberger
a881bd41eb refactor(outbound): dedupe plugin outbound context 2026-02-16 01:35:46 +00:00
Onur
cd44a0d01e fix: codex and similar processes keep dying on pty, solved by refactoring process spawning (#14257)
* exec: clean up PTY resources on timeout and exit

* cli: harden resume cleanup and watchdog stalled runs

* cli: productionize PTY and resume reliability paths

* docs: add PTY process supervision architecture plan

* docs: rewrite PTY supervision plan as pre-rewrite baseline

* docs: switch PTY supervision plan to one-go execution

* docs: add one-line root cause to PTY supervision plan

* docs: add OS contracts and test matrix to PTY supervision plan

* docs: define process-supervisor package placement and scope

* docs: tie supervisor plan to existing CI lanes

* docs: place PTY supervisor plan under src/process

* refactor(process): route exec and cli runs through supervisor

* docs(process): refresh PTY supervision plan

* wip

* fix(process): harden supervisor timeout and PTY termination

* fix(process): harden supervisor adapters env and wait handling

* ci: avoid failing formal conformance on comment permissions

* test(ui): fix cron request mock argument typing

* fix(ui): remove leftover conflict marker

* fix: supervise PTY processes (#14257) (openclaw#14257) (thanks @onutc)
2026-02-16 02:32:05 +01:00
Peter Steinberger
a73e7786e7 refactor(cron): share runnable job filter 2026-02-16 01:29:01 +00:00
Peter Steinberger
2679089e9e refactor(cron): dedupe next-run recompute loop 2026-02-16 01:27:40 +00:00
Peter Steinberger
c95a61aa9d refactor(cron): dedupe read-only load flow 2026-02-16 01:26:37 +00:00
Peter Steinberger
73a97ee255 refactor(gateway): share node invoke error handling 2026-02-16 01:25:06 +00:00
Peter Steinberger
b1dca644bc refactor(gateway): share restart request parsing 2026-02-16 01:21:54 +00:00
Peter Steinberger
b743e652c0 refactor(gateway): reuse shared validators + baseHash 2026-02-16 01:19:01 +00:00
Peter Steinberger
71cee673b2 fix(gateway): satisfy server-method lint 2026-02-16 01:15:31 +00:00
Peter Steinberger
dc5d234848 refactor(gateway): share server-method param validation 2026-02-16 01:13:52 +00:00
Peter Steinberger
a5cbd036de refactor(gateway): dedupe wizard param validation 2026-02-16 01:08:36 +00:00
Peter Steinberger
260a514467 refactor(slack): share channel config entry type 2026-02-16 01:06:18 +00:00
Peter Steinberger
067509fa44 refactor(onboarding): dedupe WhatsApp owner allowlist 2026-02-16 01:05:27 +00:00
Peter Steinberger
e84b20a527 refactor(status-issues): share enabled/configured account gate 2026-02-16 01:03:02 +00:00
Peter Steinberger
4aaafe5322 refactor(net): share hostname normalization 2026-02-16 01:01:22 +00:00
Peter Steinberger
d5ee766afe refactor(outbound): dedupe channel handler params 2026-02-16 00:59:42 +00:00
Peter Steinberger
00c91c3678 refactor(outbound): dedupe queued delivery param types 2026-02-16 00:57:28 +00:00
Peter Steinberger
4ab25a2889 refactor(outbound): reuse message gateway call 2026-02-16 00:56:28 +00:00
Advait Paliwal
14fb2c05b1 Gateway/Control UI: preserve partial output on abort (#15026)
* Gateway/Control UI: preserve partial output on abort

* fix: finalize abort partial handling and tests (#15026) (thanks @advaitpaliwal)

---------

Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
2026-02-15 16:55:28 -08:00
Peter Steinberger
57d5a8df86 refactor(outbound): dedupe directory list call 2026-02-16 00:54:37 +00:00
Peter Steinberger
b6871d9c0f refactor(outbound): dedupe attachment hydration 2026-02-16 00:52:27 +00:00
Peter Steinberger
f03ea76db3 fix(slack): tighten threadId type to satisfy lint 2026-02-16 00:49:00 +00:00
Peter Steinberger
753491ab80 refactor(slack): dedupe outbound send flow 2026-02-16 00:48:32 +00:00
Peter Steinberger
d00adfe98c refactor(signal): dedupe outbound media limit resolve 2026-02-16 00:47:19 +00:00
Peter Steinberger
2b2c3a071b refactor(imessage): dedupe outbound media limit resolve 2026-02-16 00:46:18 +00:00
Peter Steinberger
f8fbeb52b0 refactor(protocol): dedupe cron/config schemas 2026-02-16 00:46:11 +00:00
Peter Steinberger
cb46ea037f refactor(models): dedupe set default model updates 2026-02-16 00:43:15 +00:00
Peter Steinberger
dece9e8b07 refactor(update): share package.json readers 2026-02-16 00:41:28 +00:00
Peter Steinberger
32221e194a refactor(probe): share withTimeout 2026-02-16 00:39:11 +00:00
Peter Steinberger
5ecc364d55 fix(daemon): drop unused formatGatewayServiceDescription import 2026-02-16 00:37:19 +00:00
Peter Steinberger
0dbc51aa55 refactor(daemon): share service description resolve 2026-02-16 00:36:43 +00:00
Peter Steinberger
58cf37ceeb refactor(memory): reuse batch utils in gemini 2026-02-16 00:34:10 +00:00
Peter Steinberger
652318e56a refactor(media): share http error handling 2026-02-16 00:32:16 +00:00
Peter Steinberger
d8691ff4ec refactor(memory): share sync progress helpers 2026-02-16 00:29:01 +00:00
Peter Steinberger
8251f7c235 refactor(memory): dedupe batch helpers 2026-02-16 00:26:03 +00:00
Peter Steinberger
ae1880acf6 refactor(frontmatter): share openclaw manifest parsing 2026-02-16 00:23:33 +00:00
Peter Steinberger
fddf8a6f4a perf(test): fold pi extensions runtime registry tests into agents suite 2026-02-16 00:22:36 +00:00
Peter Steinberger
412c1d0af1 perf(test): fold logger import side-effects test into diagnostic suite 2026-02-16 00:21:30 +00:00
Peter Steinberger
166cf6a3e0 fix(web_fetch): cap response body before parsing 2026-02-16 01:21:11 +01:00
Peter Steinberger
fd3d452f1f fix(ci): fix ui cron test mock signature 2026-02-16 00:19:34 +00:00
Peter Steinberger
fdd0e78d1b perf(test): fold exec approvals socket defaults into main suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
60ce38d216 perf(test): drop redundant line signature unit test 2026-02-16 00:18:27 +00:00
Peter Steinberger
acb2a1ce37 perf(test): fold discord voice hardening into web media suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
ba3a0e7adb perf(test): fold gateway server utils into misc suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
3a7b1b36b6 perf(test): consolidate shared utility suites 2026-02-16 00:18:27 +00:00
Peter Steinberger
3830a4b58e perf(test): fold acp session store assertions into mapper suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
6288c51774 perf(test): fold secret equality assertions into audit extra suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
a508c34731 perf(test): fold signal daemon log parsing into probe suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
5baa08ed13 perf(test): fold model-default assertions into command utils suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
55fd88e967 perf(test): consolidate utils parsing helpers 2026-02-16 00:18:27 +00:00
Peter Steinberger
725f63f724 perf(test): fold restart recovery helper into spawn utils suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
c82dc02b4d perf(test): fold tui command parsing into tui suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
2cf060f774 perf(test): consolidate media-understanding misc suites 2026-02-16 00:18:27 +00:00
Peter Steinberger
5529473af9 perf(test): fold browser server-context helper into utils suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
5e3b211d93 perf(test): fold gmail watcher assertions into hooks install suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
3fd40fc5a3 perf(test): fold media constants assertions into mime suite 2026-02-16 00:18:27 +00:00
Peter Steinberger
f934725ccd perf(test): consolidate channel misc suites 2026-02-16 00:18:27 +00:00
Peter Steinberger
5709b30700 perf(test): consolidate config misc suites 2026-02-16 00:18:27 +00:00
Peter Steinberger
2d5004cee4 perf(test): consolidate CLI utility tests 2026-02-16 00:18:27 +00:00
Peter Steinberger
1287abe0b5 perf(test): consolidate browser utility tests 2026-02-16 00:18:27 +00:00
Peter Steinberger
a91bcd2cf4 fix(test): avoid fake-timers hang in gateway lock 2026-02-16 00:18:27 +00:00
Peter Steinberger
67bfe8fb80 perf(test): cut gateway unit suite overhead 2026-02-16 00:18:26 +00:00
Peter Steinberger
be4a490c23 refactor(test): fix update-cli env restore 2026-02-16 00:16:57 +00:00
Peter Steinberger
e9ed5febc5 refactor(test): dedupe token exchange env cleanup 2026-02-16 00:16:00 +00:00
Peter Steinberger
72baa58edd refactor(test): fix copilot env restore 2026-02-16 00:15:20 +00:00
Peter Steinberger
76015aab23 refactor(test): dedupe copilot env restores 2026-02-16 00:14:48 +00:00
Advait Paliwal
115cfb4430 gateway: add cron finished-run webhook (#14535)
* gateway: add cron finished webhook delivery

* config: allow cron webhook in runtime schema

* cron: require notify flag for webhook posts

* ui/docs: add cron notify toggle and webhook docs

* fix: harden cron webhook auth and fill notify coverage (#14535) (thanks @advaitpaliwal)

---------

Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
2026-02-15 16:14:17 -08:00
Peter Steinberger
ab000bc411 refactor(test): dedupe qianfan env restore 2026-02-16 00:13:01 +00:00
Peter Steinberger
e3a93d6705 refactor(test): dedupe safe-bins mocks 2026-02-16 00:12:23 +00:00
Peter Steinberger
7857096d29 refactor(test): reuse env snapshot in model scan 2026-02-16 00:08:35 +00:00
Peter Steinberger
cedd520f25 refactor(test): simplify state dir env helpers 2026-02-16 00:08:00 +00:00
cpojer
4bdb857eca chore: Use proper pnpm caching in one CI step. 2026-02-16 09:07:09 +09:00
Peter Steinberger
997b9ad232 refactor(test): dedupe provider api key env restore 2026-02-16 00:05:02 +00:00
Peter Steinberger
e075a33ca3 refactor(test): simplify oauth/profile env restore 2026-02-16 00:03:54 +00:00
cpojer
c07036e813 chore: Update deps. 2026-02-16 09:03:29 +09:00
Shakker
b562aa6625 fix(gateway): keep boot sessions ephemeral without remapping main 2026-02-16 00:03:21 +00:00
Shakker
fe73878dfc fix(gateway): preserve session mapping across gateway restarts 2026-02-16 00:03:21 +00:00
Peter Steinberger
ee2fa5f411 refactor(test): reuse env snapshots in unit suites 2026-02-16 00:02:32 +00:00
Peter Steinberger
07dea4c6cc refactor(test): dedupe auth choice env cleanup 2026-02-15 23:59:28 +00:00
Peter Steinberger
7bb0b7d1fc refactor(test): simplify config io env snapshot 2026-02-15 23:58:06 +00:00
Peter Steinberger
a90e007d50 refactor(test): reuse env snapshot in gateway ws harness 2026-02-15 23:56:57 +00:00
Peter Steinberger
94e84e6f75 refactor(test): clean up gateway tool env restore 2026-02-15 23:56:06 +00:00
Peter Steinberger
e9c8540e21 refactor(test): simplify model auth env restore 2026-02-15 23:55:11 +00:00
Peter Steinberger
961ca61b0e refactor(test): dedupe onboard auth env cleanup 2026-02-15 23:53:55 +00:00
Peter Steinberger
f809ff5e55 refactor(test): reuse env snapshot helper 2026-02-15 23:51:24 +00:00
Peter Steinberger
d27a763eec refactor(test): reuse env helper in temp home harness 2026-02-15 23:42:20 +00:00
Peter Steinberger
abd009b092 refactor(test): dedupe openresponses server setup 2026-02-15 23:34:52 +00:00
Peter Steinberger
f0e373b82e refactor(test): simplify state dir env restore 2026-02-15 23:34:02 +00:00
Peter Steinberger
35ab521e07 refactor(test): simplify voicewake env cleanup 2026-02-15 23:34:02 +00:00
Peter Steinberger
d8d9d3724f docs(agents): add GHSA patch/publish notes 2026-02-16 00:31:51 +01:00
Peter Steinberger
e3445f59c9 docs(changelog): note inter-session provenance security fix 2026-02-16 00:31:51 +01:00
Peter Steinberger
a68ed3f64c refactor(test): reuse env snapshots in gateway call tests 2026-02-15 23:22:58 +00:00
Peter Steinberger
31980bcaf1 refactor(test): dedupe gateway env restores 2026-02-15 23:18:16 +00:00
Peter Steinberger
70f86e326d refactor(test): reuse shared env snapshots 2026-02-15 23:15:07 +00:00
Peter Steinberger
bed0e07620 fix(cli): clear plugin manifest cache after install 2026-02-15 23:14:42 +00:00
Peter Steinberger
632b71c7f8 fix(test): avoid inheriting process.env in nix config e2e 2026-02-15 23:14:42 +00:00
Peter Steinberger
eef13235ad fix(test): make sessions_spawn e2e harness ordering stable 2026-02-15 23:14:42 +00:00
Peter Steinberger
89155aa6c6 fix(test): load sessions_spawn harness before tools 2026-02-15 23:14:42 +00:00
Peter Steinberger
bbcbabab74 fix(ci): repair e2e mocks and tool schemas 2026-02-15 23:14:42 +00:00
Peter Steinberger
0e2d8b8a1e perf(test): consolidate channel action suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
c5288300a1 perf(test): consolidate reply flow suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
a7f6c95675 perf(test): consolidate slack monitor suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
74294a4653 perf(test): consolidate web auto-reply suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
c59a472ca2 perf(test): consolidate memory tool e2e suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
722bfaa9c9 perf(test): consolidate reply plumbing/state suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
37086d0c3e perf(test): consolidate sessions tool e2e suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
a1c50b4ee3 perf(test): consolidate channel plugin suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
d75cd40787 perf(test): consolidate reply utility suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
34b088ede6 perf(test): consolidate infra outbound suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
36b5f0c9a8 perf(test): consolidate gateway server-methods suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
704c8ed530 perf(test): consolidate sessions config suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
2158b09b9d perf(test): consolidate discord monitor utils 2026-02-15 23:14:42 +00:00
Peter Steinberger
ed276d3e50 perf(test): consolidate inbound reply suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
53ec78319d perf(test): consolidate session suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
51709c63fe perf(test): consolidate model selection suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
f8925b7588 perf(test): consolidate reply commands suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
023091ded3 perf(test): consolidate slack tool-result suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
ce922915ab perf(test): consolidate telegram send suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
f749365b1c perf(test): consolidate telegram create bot suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
4fc72226fa perf(test): speed up slack slash suite 2026-02-15 23:14:42 +00:00
Peter Steinberger
def74465eb perf(test): consolidate runReplyAgent suites 2026-02-15 23:14:42 +00:00
Peter Steinberger
a91553c7cf perf(slack): consolidate slash tests 2026-02-15 23:14:42 +00:00
Peter Steinberger
65ea200c31 refactor(test): share env var helpers 2026-02-15 23:12:57 +00:00
Peter Steinberger
0b56472cf5 refactor(test): dedupe ios/android gateway client id tests 2026-02-15 23:07:50 +00:00
Peter Steinberger
8ba16a894f refactor(test): reuse withGatewayServer in auth/http suites 2026-02-15 23:06:34 +00:00
Peter Steinberger
99909f7bc7 refactor(test): share gateway server start helper 2026-02-15 23:02:27 +00:00
Peter Steinberger
1b455b6d9f refactor(test): dedupe gateway hooks server setup 2026-02-15 22:43:27 +00:00
820 changed files with 37490 additions and 29136 deletions

View File

@@ -6,14 +6,14 @@ on:
pull_request:
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# Detect docs-only changes to skip heavy jobs (test, build, Windows, macOS, Android).
# Lint and format always run. Fail-safe: if detection fails, run everything.
docs-scope:
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
docs_only: ${{ steps.check.outputs.docs_only }}
docs_changed: ${{ steps.check.outputs.docs_changed }}
@@ -33,7 +33,7 @@ jobs:
changed-scope:
needs: [docs-scope]
if: needs.docs-scope.outputs.docs_only != 'true'
runs-on: ubuntu-latest
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
run_node: ${{ steps.scope.outputs.run_node }}
run_macos: ${{ steps.scope.outputs.run_macos }}
@@ -672,7 +672,8 @@ jobs:
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
# setup-android's sdkmanager currently crashes on JDK 21 in CI.
java-version: 17
- name: Setup Android SDK
uses: android-actions/setup-android@v3

View File

@@ -13,6 +13,10 @@ on:
- ".agents/**"
- "skills/**"
concurrency:
group: docker-release-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

View File

@@ -108,6 +108,7 @@ jobs:
- name: Comment on PR (informational)
if: steps.drift.outputs.drift == 'true'
continue-on-error: true
uses: actions/github-script@v7
with:
script: |

View File

@@ -7,8 +7,8 @@ on:
workflow_dispatch:
concurrency:
group: install-smoke-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
group: install-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
docs-scope:
@@ -33,19 +33,17 @@ jobs:
- name: Checkout CLI
uses: actions/checkout@v4
- name: Setup pnpm (corepack retry)
run: |
set -euo pipefail
corepack enable
for attempt in 1 2 3; do
if corepack prepare pnpm@10.23.0 --activate; then
pnpm -v
exit 0
fi
echo "corepack prepare failed (attempt $attempt/3). Retrying..."
sleep $((attempt * 10))
done
exit 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
check-latest: true
- name: Setup pnpm + cache store
uses: ./.github/actions/setup-pnpm-store-cache
with:
pnpm-version: "10.23.0"
cache-key-suffix: "node22"
- name: Install pnpm deps (minimal)
run: pnpm install --ignore-scripts --frozen-lockfile

View File

@@ -14,8 +14,8 @@ on:
- scripts/sandbox-common-setup.sh
concurrency:
group: sandbox-common-smoke-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
group: sandbox-common-smoke-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
sandbox-common-smoke:

View File

@@ -6,8 +6,8 @@ on:
branches: [main]
concurrency:
group: workflow-sanity-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
group: workflow-sanity-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
no-tabs:

View File

@@ -119,6 +119,19 @@
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
## GHSA (Repo Advisory) Patch/Publish
- Fetch: `gh api /repos/openclaw/openclaw/security-advisories/<GHSA>`
- Latest npm: `npm view openclaw version --userconfig "$(mktemp)"`
- Private fork PRs must be closed:
`fork=$(gh api /repos/openclaw/openclaw/security-advisories/<GHSA> | jq -r .private_fork.full_name)`
`gh pr list -R "$fork" --state open` (must be empty)
- Description newline footgun: write Markdown via heredoc to `/tmp/ghsa.desc.md` (no `"\\n"` strings)
- Build patch JSON via jq: `jq -n --rawfile desc /tmp/ghsa.desc.md '{summary,severity,description:$desc,vulnerabilities:[...]}' > /tmp/ghsa.patch.json`
- Patch + publish: `gh api -X PATCH /repos/openclaw/openclaw/security-advisories/<GHSA> --input /tmp/ghsa.patch.json` (publish = include `"state":"published"`; no `/publish` endpoint)
- If publish fails (HTTP 422): missing `severity`/`description`/`vulnerabilities[]`, or private fork has open PRs
- Verify: re-fetch; ensure `state=published`, `published_at` set; `jq -r .description | rg '\\\\n'` returns nothing
## Troubleshooting
- Rebrand/migration issues or legacy config/service warnings: run `openclaw doctor` (see `docs/gateway/doctor.md`).
@@ -182,3 +195,39 @@
- Publish: `npm publish --access public --otp="<otp>"` (run from the package dir).
- Verify without local npmrc side effects: `npm view <pkg> version --userconfig "$(mktemp)"`.
- Kill the tmux session after publish.
## Plugin Release Fast Path (no core `openclaw` publish)
- Release only already-on-npm plugins. Source list is in `docs/reference/RELEASING.md` under "Current npm plugin list".
- Run all CLI `op` calls and `npm publish` inside tmux to avoid hangs/interruption:
- `tmux new -d -s release-plugins-$(date +%Y%m%d-%H%M%S)`
- `eval "$(op signin --account my.1password.com)"`
- 1Password helpers:
- password used by `npm login`:
`op item get Npmjs --format=json | jq -r '.fields[] | select(.id=="password").value'`
- OTP:
`op read 'op://Private/Npmjs/one-time password?attribute=otp'`
- Fast publish loop (local helper script in `/tmp` is fine; keep repo clean):
- compare local plugin `version` to `npm view <name> version`
- only run `npm publish --access public --otp="<otp>"` when versions differ
- skip if package is missing on npm or version already matches.
- Keep `openclaw` untouched: never run publish from repo root unless explicitly requested.
- Post-check for each release:
- per-plugin: `npm view @openclaw/<name> version --userconfig "$(mktemp)"` should be `2026.2.16`
- core guard: `npm view openclaw version --userconfig "$(mktemp)"` should stay at previous version unless explicitly requested.
## Changelog Release Notes
- When cutting a mac release with beta GitHub prerelease:
- Tag `vYYYY.M.D-beta.N` from the release commit (example: `v2026.2.15-beta.1`).
- Create prerelease with title `openclaw YYYY.M.D-beta.N`.
- Use release notes from `CHANGELOG.md` version section (`Changes` + `Fixes`, no title duplicate).
- Attach at least `OpenClaw-YYYY.M.D.zip` and `OpenClaw-YYYY.M.D.dSYM.zip`; include `.dmg` if available.
- Keep top version entries in `CHANGELOG.md` sorted by impact:
- `### Changes` first.
- `### Fixes` deduped and ranked with user-facing fixes first.
- Before tagging/publishing, run:
- `node --import tsx scripts/release-check.ts`
- `pnpm release:check`
- `pnpm test:install:smoke` or `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` for non-root smoke path.

View File

@@ -2,42 +2,76 @@
Docs: https://docs.openclaw.ai
## 2026.2.15 (Unreleased)
## 2026.2.16 (Unreleased)
### Changes
- Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.
- Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.
- Plugins: expose `llm_input` and `llm_output` hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.
- Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set `agents.defaults.subagents.maxSpawnDepth: 2` to allow sub-agents to spawn their own children. Includes `maxChildrenPerAgent` limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.
- Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.
- Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.
- Cron/Gateway: add finished-run webhook delivery toggle (`notify`) and dedicated webhook auth token support (`cron.webhookToken`) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.
- Cron/Gateway: separate per-job webhook delivery (`delivery.mode = "webhook"`) from announce delivery, enforce valid HTTP(S) webhook URLs, and keep a temporary legacy `notify + cron.webhook` fallback for stored jobs. (#17901) Thanks @advaitpaliwal.
- Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.
### Fixes
- Web UI/Agents: hide `BOOTSTRAP.md` in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.
- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx.
- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
- Discord: send initial content when creating non-forum threads so `thread-create` content is delivered. (#18117) Thanks @zerone0x.
- Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.
- Security/Sessions: create new session transcript JSONL files with user-only (`0o600`) permissions and extend `openclaw security audit --fix` to remediate existing transcript file permissions.
- Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.
- Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.
- Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.
- Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n.
- Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (`allowInsecureAuth` / `dangerouslyDisableDeviceAuth`) when device identity is unavailable, preventing false `missing scope` failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.
- LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.
- Skills/Security: restrict `download` installer `targetDir` to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.
- Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.
- Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.
- Infra/Fetch: ensure foreign abort-signal listener cleanup never masks original fetch successes/failures, while still preventing detached-finally unhandled rejection noise in `wrapFetchWithAbortSignal`. Thanks @Jackten.
- Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving `passwordFile` path exemptions, preventing accidental redaction of non-secret config values like `maxTokens` and IRC password-file paths. (#16042) Thanks @akramcodez.
- Gateway/Config: prevent `config.patch` object-array merges from falling back to full-array replacement when some patch entries lack `id`, so partial `agents.list` updates no longer drop unrelated agents. (#17989) Thanks @stakeswky.
- Dev tooling: harden git `pre-commit` hook against option injection from malicious filenames (for example `--force`), preventing accidental staging of ignored files. Thanks @mrthankyou.
- Gateway/Agent: reject malformed `agent:`-prefixed session keys (for example, `agent:main`) in `agent` and `agent.identity.get` instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz.
- Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing `script-src 'self'`. Thanks @Adam55A-code.
- Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.
- Agents/Sandbox: clarify system prompt path guidance so sandbox `bash/exec` uses container paths (for example `/workspace`) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.
- Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.
- Agents/Context: derive `lookupContextTokens()` from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.
- Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.
- Memory/FTS: make `buildFtsQuery` Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.
- Auto-reply/Compaction: resolve `memory/YYYY-MM-DD.md` placeholders with timezone-aware runtime dates and append a `Current time:` line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.
- Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.
- Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.
- Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.
- Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.
- Subagents/Models: preserve `agents.defaults.model.fallbacks` when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.
- Agents/Models: probe the primary model when its auth-profile cooldown is near expiry (with per-provider throttling), so runs recover from temporary rate limits without staying on fallback models until restart. (#17478) Thanks @PlayerGhost.
- Telegram: omit `message_thread_id` for DM sends/draft previews and keep forum-topic handling (`id=1` general omitted, non-general kept), preventing DM failures with `400 Bad Request: message thread not found`. (#10942) Thanks @garnetlyx.
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
- Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.
- Telegram: keep draft-stream preview replies attached to the user message for `replyToMode: "all"` in groups and DMs, preserving threaded reply context from preview through finalization. (#17880) Thanks @yinghaosang.
- Telegram: disable block streaming when `channels.telegram.streamMode` is `off`, preventing newline/content-block replies from splitting into multiple messages. (#17679) Thanks @saivarunk.
- Telegram: route non-abort slash commands on the normal chat/topic sequential lane while keeping true abort requests (`/stop`, `stop`) on the control lane, preventing command/reply race conditions from control-lane bypass. (#17899) Thanks @obviyus.
- Telegram: prevent streaming final replies from being overwritten by later final/error payloads, and suppress fallback tool-error warnings when a recovered assistant answer already exists after tool calls. (#17883) Thanks @Marvae and @obviyus.
- Discord: preserve channel session continuity when runtime payloads omit `message.channelId` by falling back to event/raw `channel_id` values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as `sessionKey=unknown`. (#17622) Thanks @shakkernerd.
- Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with `_2` suffixes. (#17365) Thanks @seewhyme.
- Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.
- Web UI/Agents: hide `BOOTSTRAP.md` in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.
- Memory/QMD: scope managed collection names per agent and precreate glob-backed collection directories before registration, preventing cross-agent collection clobbering and startup ENOENT failures in fresh workspaces. (#17194) Thanks @jonathanadams96.
- Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie.
- Auto-reply/TTS: keep tool-result media delivery enabled in group chats and native command sessions (while still suppressing tool summary text) so `NO_REPLY` follow-ups do not drop successful TTS audio. (#17991) Thanks @zerone0x.
- Cron: infer `payload.kind="agentTurn"` for model-only `cron.update` payload patches, so partial agent-turn updates do not fail validation when `kind` is omitted. (#15664) Thanks @rodrigouroz.
- Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope.
- TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.
- TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.
- TUI: suppress false `(no output)` placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.
- TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.
- Auto-reply/WhatsApp/TUI/Web: when a final assistant message is `NO_REPLY` and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show `NO_REPLY` placeholders. (#7010) Thanks @Morrowind-Xie.
- Gateway/Chat: harden `chat.send` inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.
- Gateway/Send: return an actionable error when `send` targets internal-only `webchat`, guiding callers to use `chat.send` or a deliverable channel. (#15703) Thanks @rodrigouroz.
- Gateway/Agent: reject malformed `agent:`-prefixed session keys (for example, `agent:main`) in `agent` and `agent.identity.get` instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.
- Gateway/Security: redact sensitive session/path details from `status` responses for non-admin clients; full details remain available to `operator.admin`. (#8590) Thanks @fr33d3m0n.
- Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.
- Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.
- Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.
- CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.
- Telegram: replace inbound `<media:audio>` placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.
- Telegram: retry inbound media `getFile` calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.
- Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.
- Cron: infer `payload.kind="agentTurn"` for model-only `cron.update` payload patches, so partial agent-turn updates do not fail validation when `kind` is omitted. (#15664) Thanks @rodrigouroz.
- Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.
- Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.
## 2026.2.14
@@ -52,6 +86,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Security/Sessions/Telegram: restrict session tool targeting by default to the current session tree (`tools.sessions.visibility`, default `tree`) with sandbox clamping, and pass configured per-account Telegram webhook secrets in webhook mode when no explicit override is provided. Thanks @aether-ai-agent.
- CLI/Plugins: ensure `openclaw message send` exits after successful delivery across plugin-backed channels so one-shot sends do not hang. (#16491) Thanks @yinghaosang.
- CLI/Plugins: run registered plugin `gateway_stop` hooks before `openclaw message` exits (success and failure paths), so plugin-backed channels can clean up one-shot CLI resources. (#16580) Thanks @gumadeiras.
- WhatsApp: honor per-account `dmPolicy` overrides (account-level settings now take precedence over channel defaults for inbound DMs). (#10082) Thanks @mcaxtr.
@@ -91,6 +126,7 @@ Docs: https://docs.openclaw.ai
- Gateway/Subagents: preserve queued announce items and summary state on delivery errors, retry failed announce drains, and avoid dropping unsent announcements on timeout/failure. (#16729) Thanks @Clawdette-Workspace.
- Gateway/Config: make `config.patch` merge object arrays by `id` (for example `agents.list`) instead of replacing the whole array, so partial agent updates do not silently delete unrelated agents. (#6766) Thanks @lightclient.
- Webchat/Prompts: stop injecting direct-chat `conversation_label` into inbound untrusted metadata context blocks, preventing internal label noise from leaking into visible chat replies. (#16556) Thanks @nberardi.
- Auto-reply/Prompts: include trusted inbound `message_id`, `chat_id`, `reply_to_id`, and optional `message_id_full` metadata fields so action tools (for example reactions) can target the triggering message without relying on user text. (#17662) Thanks @MaikiMolto.
- Gateway/Sessions: abort active embedded runs and clear queued session work before `sessions.reset`, returning unavailable if the run does not stop in time. (#16576) Thanks @Grynn.
- Sessions/Agents: harden transcript path resolution for mismatched agent context by preserving explicit store roots and adding safe absolute-path fallback to the correct agent sessions directory. (#16288) Thanks @robbyczgw-cla.
- Agents: add a safety timeout around embedded `session.compact()` to ensure stalled compaction runs settle and release blocked session lanes. (#16331) Thanks @BinHPdev.
@@ -106,6 +142,7 @@ Docs: https://docs.openclaw.ai
- Tools/Write/Edit: normalize structured text-block arguments for `content`/`oldText`/`newText` before filesystem edits, preventing JSON-like file corruption and false “exact text not found” misses from block-form params. (#16778) Thanks @danielpipernz.
- Ollama/Agents: avoid forcing `<final>` tag enforcement for Ollama models, which could suppress all output as `(no output)`. (#16191) Thanks @Glucksberg.
- Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238.
- Agents/Process: supervise PTY/child process lifecycles with explicit ownership, cancellation, timeouts, and deterministic cleanup, preventing Codex/Pi PTY sessions from dying or stalling on resume. (#14257) Thanks @onutc.
- Skills: watch `SKILL.md` only when refreshing skills snapshot to avoid file-descriptor exhaustion in large data trees. (#11325) Thanks @household-bard.
- Memory/QMD: make `memory status` read-only by skipping QMD boot update/embed side effects for status-only manager checks.
- Memory/QMD: keep original QMD failures when builtin fallback initialization fails (for example missing embedding API keys), instead of replacing them with fallback init errors.
@@ -119,6 +156,7 @@ Docs: https://docs.openclaw.ai
- Memory/QMD: make QMD result JSON parsing resilient to noisy command output by extracting the first JSON array from noisy `stdout`.
- Memory/QMD: treat prefixed `no results found` marker output as an empty result set in qmd JSON parsing. (#11302) Thanks @blazerui.
- Memory/QMD: avoid multi-collection `query` ranking corruption by running one `qmd query -c <collection>` per managed collection and merging by best score (also used for `search`/`vsearch` fallback-to-query). (#16740) Thanks @volarian-vai.
- Memory/QMD: rebind managed collections when existing collection metadata drifts (including sessions name-only listings), preventing non-default agents from reusing another agent's `sessions` collection path. (#17194) Thanks @jonathanadams96.
- Memory/QMD: make `openclaw memory index` verify and print the active QMD index file path/size, and fail when QMD leaves a missing or zero-byte index artifact after an update. (#16775) Thanks @Shunamxiao.
- Memory/QMD: detect null-byte `ENOTDIR` update failures, rebuild managed collections once, and retry update to self-heal corrupted collection metadata. (#12919) Thanks @jorgejhms.
- Memory/QMD/Security: add `rawKeyPrefix` support for QMD scope rules and preserve legacy `keyPrefix: "agent:..."` matching, preventing scoped deny bypass when operators match agent-prefixed session keys.
@@ -152,6 +190,7 @@ Docs: https://docs.openclaw.ai
- Security/Media: stream and bound URL-backed input media fetches to prevent memory exhaustion from oversized responses. Thanks @vincentkoc.
- Security/Skills: harden archive extraction for download-installed skills to prevent path traversal outside the target directory. Thanks @markmusson.
- Security/Slack: compute command authorization for DM slash commands even when `dmPolicy=open`, preventing unauthorized users from running privileged commands via DM. Thanks @christos-eth.
- Security/Pairing: scope pairing allowlist writes/reads to channel accounts (for example `telegram:yy`), and propagate account-aware pairing approvals so multi-account channels do not share a single per-channel pairing allowFrom store. (#17631) Thanks @crazytan.
- Security/iMessage: keep DM pairing-store identities out of group allowlist authorization (prevents cross-context command authorization). Thanks @vincentkoc.
- Security/Google Chat: deprecate `users/<email>` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc.
- Security/Google Chat: reject ambiguous shared-path webhook routing when multiple webhook targets verify successfully (prevents cross-account policy-context misrouting). Thanks @vincentkoc.
@@ -207,6 +246,7 @@ Docs: https://docs.openclaw.ai
- Docs/Hooks: update hooks documentation URLs to the new `/automation/hooks` location. (#16165) Thanks @nicholascyh.
- Security/Audit: warn when `gateway.tools.allow` re-enables default-denied tools over HTTP `POST /tools/invoke`, since this can increase RCE blast radius if the gateway is reachable.
- Security/Plugins/Hooks: harden npm-based installs by restricting specs to registry packages only, passing `--ignore-scripts` to `npm pack`, and cleaning up temp install directories.
- Security/Sessions: preserve inter-session input provenance for routed prompts so delegated/internal sessions are not treated as direct external user instructions. Thanks @anbecker.
- Feishu: stop persistent Typing reaction on NO_REPLY/suppressed runs by wiring reply-dispatcher cleanup to remove typing indicators. (#15464) Thanks @arosstale.
- Agents: strip leading empty lines from `sanitizeUserFacingText` output and normalize whitespace-only outputs to empty text. (#16158) Thanks @mcinteerj.
- BlueBubbles: gracefully degrade when Private API is disabled by filtering private-only actions, skipping private-only reactions/reply effects, and avoiding private reply markers so non-private flows remain usable. (#16002) Thanks @L-U-C-K-Y.
@@ -337,6 +377,7 @@ Docs: https://docs.openclaw.ai
- Configure/Gateway: reject literal `"undefined"`/`"null"` token input and validate gateway password prompt values to avoid invalid password-mode configs. (#13767) Thanks @omair445.
- Gateway: handle async `EPIPE` on stdout/stderr during shutdown. (#13414) Thanks @keshav55.
- Gateway/Control UI: resolve missing dashboard assets when `openclaw` is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica.
- Gateway/Control UI: keep partial assistant output visible when runs are aborted, and persist aborted partials to session transcripts for follow-up context.
- Cron: use requested `agentId` for isolated job auth resolution. (#13983) Thanks @0xRaini.
- Cron: prevent cron jobs from skipping execution when `nextRunAtMs` advances. (#14068) Thanks @WalterSumbon.
- Cron: pass `agentId` to `runHeartbeatOnce` for main-session jobs. (#14140) Thanks @ishikawa-pro.

View File

@@ -13,24 +13,33 @@ Welcome to the lobster tank! 🦞
- **Peter Steinberger** - Benevolent Dictator
- GitHub: [@steipete](https://github.com/steipete) · X: [@steipete](https://x.com/steipete)
- **Shadow** - Discord + Slack subsystem
- **Shadow** - Discord subsystem, Discord admin
- GitHub: [@thewilloftheshadow](https://github.com/thewilloftheshadow) · X: [@4shad0wed](https://x.com/4shad0wed)
- **Vignesh** - Memory (QMD), formal modeling, TUI, and Lobster
- **Vignesh** - Memory (QMD), formal modeling, TUI, IRC, and Lobster
- GitHub: [@vignesh07](https://github.com/vignesh07) · X: [@\_vgnsh](https://x.com/_vgnsh)
- **Jos** - Telegram, API, Nix mode
- GitHub: [@joshp123](https://github.com/joshp123) · X: [@jjpcodes](https://x.com/jjpcodes)
- **Ayaan Zaidi** - Telegram subsystem, iOS app
- GitHub: [@obviyus](https://github.com/obviyus) · X: [@0bviyus](https://x.com/0bviyus)
- **Tyler Yust** - Agents/subagents, cron, BlueBubbles, macOS app
- GitHub: [@tyler6204](https://github.com/tyler6204) · X: [@tyleryust](https://x.com/tyleryust)
- **Mariano Belinky** - iOS app, Security
- GitHub: [@mbelinky](https://github.com/mbelinky) · X: [@belimad](https://x.com/belimad)
- **Seb Slight** - Docs, Agent Reliability, Runtime Hardening
- GitHub: [@sebslight](https://github.com/sebslight) · X: [@sebslig](https://x.com/sebslig)
- **Christoph Nakazawa** - JS Infra
- GitHub: [@cpojer](https://github.com/cpojer) · X: [@cnakazawa](https://x.com/cnakazawa)
- **Gustavo Madeira Santana** - Multi-agents, CLI, web UI
- GitHub: [@gumadeiras](https://github.com/gumadeiras) · X: [@gumadeiras](https://x.com/gumadeiras)
- **Maximilian Nussbaumer** - DevOps, CI, Code Sanity
- GitHub: [@quotentiroler](https://github.com/quotentiroler) · X: [@quotentiroler](https://x.com/quotentiroler)
## How to Contribute
1. **Bugs & small fixes** → Open a PR!

View File

@@ -140,6 +140,74 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.14/OpenClaw-2026.2.14.zip" length="22914034" type="application/octet-stream" sparkle:edSignature="lR3nuq46/akMIN8RFDpMkTE0VOVoDVG53Xts589LryMGEtUvJxRQDtHBXfx7ZvToTq6CFKG+L5Kq/4rUspMoAQ=="/>
</item>
<item>
<title>2026.2.15</title>
<pubDate>Mon, 16 Feb 2026 05:04:34 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>11213</sparkle:version>
<sparkle:shortVersionString>2026.2.15</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.15</h2>
<h3>Changes</h3>
<ul>
<li>Discord: unlock rich interactive agent prompts with Components v2 (buttons, selects, modals, and attachment-backed file blocks) so for native interaction through Discord. Thanks @thewilloftheshadow.</li>
<li>Discord: components v2 UI + embeds passthrough + exec approval UX refinements (CV2 containers, button layout, Discord-forwarding skip). Thanks @thewilloftheshadow.</li>
<li>Plugins: expose <code>llm_input</code> and <code>llm_output</code> hook payloads so extensions can observe prompt/input context and model output usage details. (#16724) Thanks @SecondThread.</li>
<li>Subagents: nested sub-agents (sub-sub-agents) with configurable depth. Set <code>agents.defaults.subagents.maxSpawnDepth: 2</code> to allow sub-agents to spawn their own children. Includes <code>maxChildrenPerAgent</code> limit (default 5), depth-aware tool policy, and proper announce chain routing. (#14447) Thanks @tyler6204.</li>
<li>Slack/Discord/Telegram: add per-channel ack reaction overrides (account/channel-level) to support platform-specific emoji formats. (#17092) Thanks @zerone0x.</li>
<li>Cron/Gateway: add finished-run webhook delivery toggle (<code>notify</code>) and dedicated webhook auth token support (<code>cron.webhookToken</code>) for outbound cron webhook posts. (#14535) Thanks @advaitpaliwal.</li>
<li>Channels: deduplicate probe/token resolution base types across core + extensions while preserving per-channel error typing. (#16986) Thanks @iyoda and @thewilloftheshadow.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Security: replace deprecated SHA-1 sandbox configuration hashing with SHA-256 for deterministic sandbox cache identity and recreation checks. Thanks @kexinoh.</li>
<li>Security/Logging: redact Telegram bot tokens from error messages and uncaught stack traces to prevent accidental secret leakage into logs. Thanks @aether-ai-agent.</li>
<li>Sandbox/Security: block dangerous sandbox Docker config (bind mounts, host networking, unconfined seccomp/apparmor) to prevent container escape via config injection. Thanks @aether-ai-agent.</li>
<li>Sandbox: preserve array order in config hashing so order-sensitive Docker/browser settings trigger container recreation correctly. Thanks @kexinoh.</li>
<li>Gateway/Security: redact sensitive session/path details from <code>status</code> responses for non-admin clients; full details remain available to <code>operator.admin</code>. (#8590) Thanks @fr33d3m0n.</li>
<li>Gateway/Control UI: preserve requested operator scopes for Control UI bypass modes (<code>allowInsecureAuth</code> / <code>dangerouslyDisableDeviceAuth</code>) when device identity is unavailable, preventing false <code>missing scope</code> failures on authenticated LAN/HTTP operator sessions. (#17682) Thanks @leafbird.</li>
<li>LINE/Security: fail closed on webhook startup when channel token or channel secret is missing, and treat LINE accounts as configured only when both are present. (#17587) Thanks @davidahmann.</li>
<li>Skills/Security: restrict <code>download</code> installer <code>targetDir</code> to the per-skill tools directory to prevent arbitrary file writes. Thanks @Adam55A-code.</li>
<li>Skills/Linux: harden go installer fallback on apt-based systems by handling root/no-sudo environments safely, doing best-effort apt index refresh, and returning actionable errors instead of failing with spawn errors. (#17687) Thanks @mcrolly.</li>
<li>Web Fetch/Security: cap downloaded response body size before HTML parsing to prevent memory exhaustion from oversized or deeply nested pages. Thanks @xuemian168.</li>
<li>Config/Gateway: make sensitive-key whitelist suffix matching case-insensitive while preserving <code>passwordFile</code> path exemptions, preventing accidental redaction of non-secret config values like <code>maxTokens</code> and IRC password-file paths. (#16042) Thanks @akramcodez.</li>
<li>Dev tooling: harden git <code>pre-commit</code> hook against option injection from malicious filenames (for example <code>--force</code>), preventing accidental staging of ignored files. Thanks @mrthankyou.</li>
<li>Gateway/Agent: reject malformed <code>agent:</code>-prefixed session keys (for example, <code>agent:main</code>) in <code>agent</code> and <code>agent.identity.get</code> instead of silently resolving them to the default agent, preventing accidental cross-session routing. (#15707) Thanks @rodrigouroz.</li>
<li>Gateway/Chat: harden <code>chat.send</code> inbound message handling by rejecting null bytes, stripping unsafe control characters, and normalizing Unicode to NFC before dispatch. (#8593) Thanks @fr33d3m0n.</li>
<li>Gateway/Send: return an actionable error when <code>send</code> targets internal-only <code>webchat</code>, guiding callers to use <code>chat.send</code> or a deliverable channel. (#15703) Thanks @rodrigouroz.</li>
<li>Control UI: prevent stored XSS via assistant name/avatar by removing inline script injection, serving bootstrap config as JSON, and enforcing <code>script-src 'self'</code>. Thanks @Adam55A-code.</li>
<li>Agents/Security: sanitize workspace paths before embedding into LLM prompts (strip Unicode control/format chars) to prevent instruction injection via malicious directory names. Thanks @aether-ai-agent.</li>
<li>Agents/Sandbox: clarify system prompt path guidance so sandbox <code>bash/exec</code> uses container paths (for example <code>/workspace</code>) while file tools keep host-bridge mapping, avoiding first-attempt path misses from host-only absolute paths in sandbox command execution. (#17693) Thanks @app/juniordevbot.</li>
<li>Agents/Context: apply configured model <code>contextWindow</code> overrides after provider discovery so <code>lookupContextTokens()</code> honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07.</li>
<li>Agents/Context: derive <code>lookupContextTokens()</code> from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07.</li>
<li>Agents/OpenAI: force <code>store=true</code> for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07.</li>
<li>Memory/FTS: make <code>buildFtsQuery</code> Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471.</li>
<li>Auto-reply/Compaction: resolve <code>memory/YYYY-MM-DD.md</code> placeholders with timezone-aware runtime dates and append a <code>Current time:</code> line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07.</li>
<li>Agents: return an explicit timeout error reply when an embedded run times out before producing any payloads, preventing silent dropped turns during slow cache-refresh transitions. (#16659) Thanks @liaosvcaf and @vignesh07.</li>
<li>Group chats: always inject group chat context (name, participants, reply guidance) into the system prompt on every turn, not just the first. Prevents the model from losing awareness of which group it's in and incorrectly using the message tool to send to the same group. (#14447) Thanks @tyler6204.</li>
<li>Browser/Agents: when browser control service is unavailable, return explicit non-retry guidance (instead of "try again") so models do not loop on repeated browser tool calls until timeout. (#17673) Thanks @austenstone.</li>
<li>Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber.</li>
<li>Subagents/Models: preserve <code>agents.defaults.model.fallbacks</code> when subagent sessions carry a model override, so subagent runs fail over to configured fallback models instead of retrying only the overridden primary model.</li>
<li>Telegram: omit <code>message_thread_id</code> for DM sends/draft previews and keep forum-topic handling (<code>id=1</code> general omitted, non-general kept), preventing DM failures with <code>400 Bad Request: message thread not found</code>. (#10942) Thanks @garnetlyx.</li>
<li>Telegram: replace inbound <code><media:audio></code> placeholder with successful preflight voice transcript in message body context, preventing placeholder-only prompt bodies for mention-gated voice messages. (#16789) Thanks @Limitless2023.</li>
<li>Telegram: retry inbound media <code>getFile</code> calls (3 attempts with backoff) and gracefully fall back to placeholder-only processing when retries fail, preventing dropped voice/media messages on transient Telegram network errors. (#16154) Thanks @yinghaosang.</li>
<li>Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus.</li>
<li>Discord: preserve channel session continuity when runtime payloads omit <code>message.channelId</code> by falling back to event/raw <code>channel_id</code> values for routing/session keys, so same-channel messages keep history across turns/restarts. Also align diagnostics so active Discord runs no longer appear as <code>sessionKey=unknown</code>. (#17622) Thanks @shakkernerd.</li>
<li>Discord: dedupe native skill commands by skill name in multi-agent setups to prevent duplicated slash commands with <code>_2</code> suffixes. (#17365) Thanks @seewhyme.</li>
<li>Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu.</li>
<li>Web UI/Agents: hide <code>BOOTSTRAP.md</code> in the Agents Files list after onboarding is completed, avoiding confusing missing-file warnings for completed workspaces. (#17491) Thanks @gumadeiras.</li>
<li>Auto-reply/WhatsApp/TUI/Web: when a final assistant message is <code>NO_REPLY</code> and a messaging tool send succeeded, mirror the delivered messaging-tool text into session-visible assistant output so TUI/Web no longer show <code>NO_REPLY</code> placeholders. (#7010) Thanks @Morrowind-Xie.</li>
<li>Cron: infer <code>payload.kind="agentTurn"</code> for model-only <code>cron.update</code> payload patches, so partial agent-turn updates do not fail validation when <code>kind</code> is omitted. (#15664) Thanks @rodrigouroz.</li>
<li>TUI: make searchable-select filtering and highlight rendering ANSI-aware so queries ignore hidden escape codes and no longer corrupt ANSI styling sequences during match highlighting. (#4519) Thanks @bee4come.</li>
<li>TUI/Windows: coalesce rapid single-line submit bursts in Git Bash into one multiline message as a fallback when bracketed paste is unavailable, preventing pasted multiline text from being split into multiple sends. (#4986) Thanks @adamkane.</li>
<li>TUI: suppress false <code>(no output)</code> placeholders for non-local empty final events during concurrent runs, preventing external-channel replies from showing empty assistant bubbles while a local run is still streaming. (#5782) Thanks @LagWizard and @vignesh07.</li>
<li>TUI: preserve copy-sensitive long tokens (URLs/paths/file-like identifiers) during wrapping and overflow sanitization so wrapped output no longer inserts spaces that corrupt copy/paste values. (#17515, #17466, #17505) Thanks @abe238, @trevorpan, and @JasonCry.</li>
<li>CLI/Build: make legacy daemon CLI compatibility shim generation tolerant of minimal tsdown daemon export sets, while preserving restart/register compatibility aliases and surfacing explicit errors for unavailable legacy daemon commands. Thanks @vignesh07.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.15/OpenClaw-2026.2.15.zip" length="22896513" type="application/octet-stream" sparkle:edSignature="MLGsd2NeHXFRH1Or0bFQnAjqfuuJDuhl1mvKFIqTQcRvwbeyvOyyLXrqSbmaOgJR3wBQBKLs6jYQ9dQ/3R8RCg=="/>
</item>
<item>
<title>2026.2.13</title>
<pubDate>Sat, 14 Feb 2026 04:30:23 +0100</pubDate>
@@ -241,101 +309,5 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.13/OpenClaw-2026.2.13.zip" length="22902077" type="application/octet-stream" sparkle:edSignature="RpkwlPtB2yN7UOYZWfthV5grhDUcbhcHMeicdRA864Vo/P0Hnq5aHKmSvcbWkjHut96TC57bX+AeUrL7txpLCg=="/>
</item>
<item>
<title>2026.2.12</title>
<pubDate>Fri, 13 Feb 2026 03:17:54 +0100</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>9500</sparkle:version>
<sparkle:shortVersionString>2026.2.12</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.2.12</h2>
<h3>Changes</h3>
<ul>
<li>CLI: add <code>openclaw logs --local-time</code> to display log timestamps in local timezone. (#13818) Thanks @xialonglee.</li>
<li>Telegram: render blockquotes as native <code><blockquote></code> tags instead of stripping them. (#14608)</li>
<li>Config: avoid redacting <code>maxTokens</code>-like fields during config snapshot redaction, preventing round-trip validation failures in <code>/config</code>. (#14006) Thanks @constansino.</li>
</ul>
<h3>Breaking</h3>
<ul>
<li>Hooks: <code>POST /hooks/agent</code> now rejects payload <code>sessionKey</code> overrides by default. To keep fixed hook context, set <code>hooks.defaultSessionKey</code> (recommended with <code>hooks.allowedSessionKeyPrefixes: ["hook:"]</code>). If you need legacy behavior, explicitly set <code>hooks.allowRequestSessionKey: true</code>. Thanks @alpernae for reporting.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Gateway/OpenResponses: harden URL-based <code>input_file</code>/<code>input_image</code> handling with explicit SSRF deny policy, hostname allowlists (<code>files.urlAllowlist</code> / <code>images.urlAllowlist</code>), per-request URL input caps (<code>maxUrlParts</code>), blocked-fetch audit logging, and regression coverage/docs updates.</li>
<li>Security: fix unauthenticated Nostr profile API remote config tampering. (#13719) Thanks @coygeek.</li>
<li>Security: remove bundled soul-evil hook. (#14757) Thanks @Imccccc.</li>
<li>Security/Audit: add hook session-routing hardening checks (<code>hooks.defaultSessionKey</code>, <code>hooks.allowRequestSessionKey</code>, and prefix allowlists), and warn when HTTP API endpoints allow explicit session-key routing.</li>
<li>Security/Sandbox: confine mirrored skill sync destinations to the sandbox <code>skills/</code> root and stop using frontmatter-controlled skill names as filesystem destination paths. Thanks @1seal.</li>
<li>Security/Web tools: treat browser/web content as untrusted by default (wrapped outputs for browser snapshot/tabs/console and structured external-content metadata for web tools), and strip <code>toolResult.details</code> from model-facing transcript/compaction inputs to reduce prompt-injection replay risk.</li>
<li>Security/Hooks: harden webhook and device token verification with shared constant-time secret comparison, and add per-client auth-failure throttling for hook endpoints (<code>429</code> + <code>Retry-After</code>). Thanks @akhmittra.</li>
<li>Security/Browser: require auth for loopback browser control HTTP routes, auto-generate <code>gateway.auth.token</code> when browser control starts without auth, and add a security-audit check for unauthenticated browser control. Thanks @tcusolle.</li>
<li>Sessions/Gateway: harden transcript path resolution and reject unsafe session IDs/file paths so session operations stay within agent sessions directories. Thanks @akhmittra.</li>
<li>Gateway: raise WS payload/buffer limits so 5,000,000-byte image attachments work reliably. (#14486) Thanks @0xRaini.</li>
<li>Logging/CLI: use local timezone timestamps for console prefixing, and include <code>±HH:MM</code> offsets when using <code>openclaw logs --local-time</code> to avoid ambiguity. (#14771) Thanks @0xRaini.</li>
<li>Gateway: drain active turns before restart to prevent message loss. (#13931) Thanks @0xRaini.</li>
<li>Gateway: auto-generate auth token during install to prevent launchd restart loops. (#13813) Thanks @cathrynlavery.</li>
<li>Gateway: prevent <code>undefined</code>/missing token in auth config. (#13809) Thanks @asklee-klawd.</li>
<li>Gateway: handle async <code>EPIPE</code> on stdout/stderr during shutdown. (#13414) Thanks @keshav55.</li>
<li>Gateway/Control UI: resolve missing dashboard assets when <code>openclaw</code> is installed globally via symlink-based Node managers (nvm/fnm/n/Homebrew). (#14919) Thanks @aynorica.</li>
<li>Cron: use requested <code>agentId</code> for isolated job auth resolution. (#13983) Thanks @0xRaini.</li>
<li>Cron: prevent cron jobs from skipping execution when <code>nextRunAtMs</code> advances. (#14068) Thanks @WalterSumbon.</li>
<li>Cron: pass <code>agentId</code> to <code>runHeartbeatOnce</code> for main-session jobs. (#14140) Thanks @ishikawa-pro.</li>
<li>Cron: re-arm timers when <code>onTimer</code> fires while a job is still executing. (#14233) Thanks @tomron87.</li>
<li>Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.</li>
<li>Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.</li>
<li>Cron: prevent one-shot <code>at</code> jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.</li>
<li>Heartbeat: prevent scheduler stalls on unexpected run errors and avoid immediate rerun loops after <code>requests-in-flight</code> skips. (#14901) Thanks @joeykrug.</li>
<li>Cron: honor stored session model overrides for isolated-agent runs while preserving <code>hooks.gmail.model</code> precedence for Gmail hook sessions. (#14983) Thanks @shtse8.</li>
<li>Logging/Browser: fall back to <code>os.tmpdir()/openclaw</code> for default log, browser trace, and browser download temp paths when <code>/tmp/openclaw</code> is unavailable.</li>
<li>WhatsApp: convert Markdown bold/strikethrough to WhatsApp formatting. (#14285) Thanks @Raikan10.</li>
<li>WhatsApp: allow media-only sends and normalize leading blank payloads. (#14408) Thanks @karimnaguib.</li>
<li>WhatsApp: default MIME type for voice messages when Baileys omits it. (#14444) Thanks @mcaxtr.</li>
<li>Telegram: handle no-text message in model picker editMessageText. (#14397) Thanks @0xRaini.</li>
<li>Telegram: surface REACTION_INVALID as non-fatal warning. (#14340) Thanks @0xRaini.</li>
<li>BlueBubbles: fix webhook auth bypass via loopback proxy trust. (#13787) Thanks @coygeek.</li>
<li>Slack: change default replyToMode from "off" to "all". (#14364) Thanks @nm-de.</li>
<li>Slack: detect control commands when channel messages start with bot mention prefixes (for example, <code>@Bot /new</code>). (#14142) Thanks @beefiker.</li>
<li>Signal: enforce E.164 validation for the Signal bot account prompt so mistyped numbers are caught early. (#15063) Thanks @Duartemartins.</li>
<li>Discord: process DM reactions instead of silently dropping them. (#10418) Thanks @mcaxtr.</li>
<li>Discord: respect replyToMode in threads. (#11062) Thanks @cordx56.</li>
<li>Heartbeat: filter noise-only system events so scheduled reminder notifications do not fire when cron runs carry only heartbeat markers. (#13317) Thanks @pvtclawn.</li>
<li>Signal: render mention placeholders as <code>@uuid</code>/<code>@phone</code> so mention gating and Clawdbot targeting work. (#2013) Thanks @alexgleason.</li>
<li>Discord: omit empty content fields for media-only messages while preserving caption whitespace. (#9507) Thanks @leszekszpunar.</li>
<li>Onboarding/Providers: add Z.AI endpoint-specific auth choices (<code>zai-coding-global</code>, <code>zai-coding-cn</code>, <code>zai-global</code>, <code>zai-cn</code>) and expand default Z.AI model wiring. (#13456) Thanks @tomsun28.</li>
<li>Onboarding/Providers: update MiniMax API default/recommended models from M2.1 to M2.5, add M2.5/M2.5-Lightning model entries, and include <code>minimax-m2.5</code> in modern model filtering. (#14865) Thanks @adao-max.</li>
<li>Ollama: use configured <code>models.providers.ollama.baseUrl</code> for model discovery and normalize <code>/v1</code> endpoints to the native Ollama API root. (#14131) Thanks @shtse8.</li>
<li>Voice Call: pass Twilio stream auth token via <code><Parameter></code> instead of query string. (#14029) Thanks @mcwigglesmcgee.</li>
<li>Feishu: pass <code>Buffer</code> directly to the Feishu SDK upload APIs instead of <code>Readable.from(...)</code> to avoid form-data upload failures. (#10345) Thanks @youngerstyle.</li>
<li>Feishu: trigger mention-gated group handling only when the bot itself is mentioned (not just any mention). (#11088) Thanks @openperf.</li>
<li>Feishu: probe status uses the resolved account context for multi-account credential checks. (#11233) Thanks @onevcat.</li>
<li>Feishu DocX: preserve top-level converted block order using <code>firstLevelBlockIds</code> when writing/appending documents. (#13994) Thanks @Cynosure159.</li>
<li>Feishu plugin packaging: remove <code>workspace:*</code> <code>openclaw</code> dependency from <code>extensions/feishu</code> and sync lockfile for install compatibility. (#14423) Thanks @jackcooper2015.</li>
<li>CLI/Wizard: exit with code 1 when <code>configure</code>, <code>agents add</code>, or interactive <code>onboard</code> wizards are canceled, so <code>set -e</code> automation stops correctly. (#14156) Thanks @0xRaini.</li>
<li>Media: strip <code>MEDIA:</code> lines with local paths instead of leaking as visible text. (#14399) Thanks @0xRaini.</li>
<li>Config/Cron: exclude <code>maxTokens</code> from config redaction and honor <code>deleteAfterRun</code> on skipped cron jobs. (#13342) Thanks @niceysam.</li>
<li>Config: ignore <code>meta</code> field changes in config file watcher. (#13460) Thanks @brandonwise.</li>
<li>Cron: use requested <code>agentId</code> for isolated job auth resolution. (#13983) Thanks @0xRaini.</li>
<li>Cron: pass <code>agentId</code> to <code>runHeartbeatOnce</code> for main-session jobs. (#14140) Thanks @ishikawa-pro.</li>
<li>Cron: prevent cron jobs from skipping execution when <code>nextRunAtMs</code> advances. (#14068) Thanks @WalterSumbon.</li>
<li>Cron: re-arm timers when <code>onTimer</code> fires while a job is still executing. (#14233) Thanks @tomron87.</li>
<li>Cron: prevent duplicate fires when multiple jobs trigger simultaneously. (#14256) Thanks @xinhuagu.</li>
<li>Cron: isolate scheduler errors so one bad job does not break all jobs. (#14385) Thanks @MarvinDontPanic.</li>
<li>Cron: prevent one-shot <code>at</code> jobs from re-firing on restart after skipped/errored runs. (#13878) Thanks @lailoo.</li>
<li>Daemon: suppress <code>EPIPE</code> error when restarting LaunchAgent. (#14343) Thanks @0xRaini.</li>
<li>Antigravity: add opus 4.6 forward-compat model and bypass thinking signature sanitization. (#14218) Thanks @jg-noncelogic.</li>
<li>Agents: prevent file descriptor leaks in child process cleanup. (#13565) Thanks @KyleChen26.</li>
<li>Agents: prevent double compaction caused by cache TTL bypassing guard. (#13514) Thanks @taw0002.</li>
<li>Agents: use last API call's cache tokens for context display instead of accumulated sum. (#13805) Thanks @akari-musubi.</li>
<li>Agents: keep followup-runner session <code>totalTokens</code> aligned with post-compaction context by using last-call usage and shared token-accounting logic. (#14979) Thanks @shtse8.</li>
<li>Hooks/Plugins: wire 9 previously unwired plugin lifecycle hooks into core runtime paths (session, compaction, gateway, and outbound message hooks). (#14882) Thanks @shtse8.</li>
<li>Hooks/Tools: dispatch <code>before_tool_call</code> and <code>after_tool_call</code> hooks from both tool execution paths with rebased conflict fixes. (#15012) Thanks @Patrick-Barletta, @Takhoffman.</li>
<li>Discord: allow channel-edit to archive/lock threads and set auto-archive duration. (#5542) Thanks @stumct.</li>
<li>Discord tests: use a partial @buape/carbon mock in slash command coverage. (#13262) Thanks @arosstale.</li>
<li>Tests: update thread ID handling in Slack message collection tests. (#14108) Thanks @swizzmagik.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.2.12/OpenClaw-2026.2.12.zip" length="22877692" type="application/octet-stream" sparkle:edSignature="TGylTM4/7Lab+qp1nuPeOAmEVV1WkafXUPub8ws0z/0mYfbVygRuiev+u3zdPjQWhLnGYTgRgKVyW+kB2+Q2BQ=="/>
</item>
</channel>
</rss>

View File

@@ -21,8 +21,8 @@ android {
applicationId = "ai.openclaw.android"
minSdk = 31
targetSdk = 36
versionCode = 202602150
versionName = "2026.2.15"
versionCode = 202602160
versionName = "2026.2.16"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")

View File

@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.15</string>
<string>2026.2.16</string>
<key>CFBundleVersion</key>
<string>20260215</string>
<string>20260216</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>

View File

@@ -17,8 +17,8 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.15</string>
<string>2026.2.16</string>
<key>CFBundleVersion</key>
<string>20260215</string>
<string>20260216</string>
</dict>
</plist>

View File

@@ -81,8 +81,8 @@ targets:
properties:
CFBundleDisplayName: OpenClaw
CFBundleIconName: AppIcon
CFBundleShortVersionString: "2026.2.15"
CFBundleVersion: "20260215"
CFBundleShortVersionString: "2026.2.16"
CFBundleVersion: "20260216"
UILaunchScreen: {}
UIApplicationSceneManifest:
UIApplicationSupportsMultipleScenes: false
@@ -130,5 +130,5 @@ targets:
path: Tests/Info.plist
properties:
CFBundleDisplayName: OpenClawTests
CFBundleShortVersionString: "2026.2.15"
CFBundleVersion: "20260215"
CFBundleShortVersionString: "2026.2.16"
CFBundleVersion: "20260216"

View File

@@ -21,6 +21,7 @@ enum CronWakeMode: String, CaseIterable, Identifiable, Codable {
enum CronDeliveryMode: String, CaseIterable, Identifiable, Codable {
case none
case announce
case webhook
var id: String {
self.rawValue

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.2.15</string>
<string>2026.2.16</string>
<key>CFBundleVersion</key>
<string>202602150</string>
<string>202602160</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -2094,7 +2094,7 @@ public struct CronJob: Codable, Sendable {
public let sessiontarget: AnyCodable
public let wakemode: AnyCodable
public let payload: AnyCodable
public let delivery: [String: AnyCodable]?
public let delivery: AnyCodable?
public let state: [String: AnyCodable]
public init(
@@ -2110,7 +2110,7 @@ public struct CronJob: Codable, Sendable {
sessiontarget: AnyCodable,
wakemode: AnyCodable,
payload: AnyCodable,
delivery: [String: AnyCodable]?,
delivery: AnyCodable?,
state: [String: AnyCodable]
) {
self.id = id
@@ -2172,7 +2172,7 @@ public struct CronAddParams: Codable, Sendable {
public let sessiontarget: AnyCodable
public let wakemode: AnyCodable
public let payload: AnyCodable
public let delivery: [String: AnyCodable]?
public let delivery: AnyCodable?
public init(
name: String,
@@ -2184,7 +2184,7 @@ public struct CronAddParams: Codable, Sendable {
sessiontarget: AnyCodable,
wakemode: AnyCodable,
payload: AnyCodable,
delivery: [String: AnyCodable]?
delivery: AnyCodable?
) {
self.name = name
self.agentid = agentid

View File

@@ -2094,7 +2094,7 @@ public struct CronJob: Codable, Sendable {
public let sessiontarget: AnyCodable
public let wakemode: AnyCodable
public let payload: AnyCodable
public let delivery: [String: AnyCodable]?
public let delivery: AnyCodable?
public let state: [String: AnyCodable]
public init(
@@ -2110,7 +2110,7 @@ public struct CronJob: Codable, Sendable {
sessiontarget: AnyCodable,
wakemode: AnyCodable,
payload: AnyCodable,
delivery: [String: AnyCodable]?,
delivery: AnyCodable?,
state: [String: AnyCodable]
) {
self.id = id
@@ -2172,7 +2172,7 @@ public struct CronAddParams: Codable, Sendable {
public let sessiontarget: AnyCodable
public let wakemode: AnyCodable
public let payload: AnyCodable
public let delivery: [String: AnyCodable]?
public let delivery: AnyCodable?
public init(
name: String,
@@ -2184,7 +2184,7 @@ public struct CronAddParams: Codable, Sendable {
sessiontarget: AnyCodable,
wakemode: AnyCodable,
payload: AnyCodable,
delivery: [String: AnyCodable]?
delivery: AnyCodable?
) {
self.name = name
self.agentid = agentid

View File

@@ -27,6 +27,8 @@ Troubleshooting: [/automation/troubleshooting](/automation/troubleshooting)
- **Main session**: enqueue a system event, then run on the next heartbeat.
- **Isolated**: run a dedicated agent turn in `cron:<jobId>`, with delivery (announce by default or none).
- Wakeups are first-class: a job can request “wake now” vs “next heartbeat”.
- Webhook posting is per job via `delivery.mode = "webhook"` + `delivery.to = "<url>"`.
- Legacy fallback remains for stored jobs with `notify: true` when `cron.webhook` is set, migrate those jobs to webhook delivery mode.
## Quick start (actionable)
@@ -99,7 +101,7 @@ A cron job is a stored record with:
- a **schedule** (when it should run),
- a **payload** (what it should do),
- optional **delivery mode** (announce or none).
- optional **delivery mode** (`announce`, `webhook`, or `none`).
- optional **agent binding** (`agentId`): run the job under a specific agent; if
missing or unknown, the gateway falls back to the default agent.
@@ -140,8 +142,9 @@ Key behaviors:
- Prompt is prefixed with `[cron:<jobId> <job name>]` for traceability.
- Each run starts a **fresh session id** (no prior conversation carry-over).
- Default behavior: if `delivery` is omitted, isolated jobs announce a summary (`delivery.mode = "announce"`).
- `delivery.mode` (isolated-only) chooses what happens:
- `delivery.mode` chooses what happens:
- `announce`: deliver a summary to the target channel and post a brief summary to the main session.
- `webhook`: POST the finished event payload to `delivery.to`.
- `none`: internal only (no delivery, no main-session summary).
- `wakeMode` controls when the main-session summary posts:
- `now`: immediate heartbeat.
@@ -163,11 +166,11 @@ Common `agentTurn` fields:
- `model` / `thinking`: optional overrides (see below).
- `timeoutSeconds`: optional timeout override.
Delivery config (isolated jobs only):
Delivery config:
- `delivery.mode`: `none` | `announce`.
- `delivery.mode`: `none` | `announce` | `webhook`.
- `delivery.channel`: `last` or a specific channel.
- `delivery.to`: channel-specific target (phone/chat/channel id).
- `delivery.to`: channel-specific target (announce) or webhook URL (webhook mode).
- `delivery.bestEffort`: avoid failing the job if announce delivery fails.
Announce delivery suppresses messaging tool sends for the run; use `delivery.channel`/`delivery.to`
@@ -192,6 +195,18 @@ Behavior details:
- The main-session summary respects `wakeMode`: `now` triggers an immediate heartbeat and
`next-heartbeat` waits for the next scheduled heartbeat.
#### Webhook delivery flow
When `delivery.mode = "webhook"`, cron posts the finished event payload to `delivery.to`.
Behavior details:
- The endpoint must be a valid HTTP(S) URL.
- No channel delivery is attempted in webhook mode.
- No main-session summary is posted in webhook mode.
- If `cron.webhookToken` is set, auth header is `Authorization: Bearer <cron.webhookToken>`.
- Deprecated fallback: stored legacy jobs with `notify: true` still post to `cron.webhook` (if configured), with a warning so you can migrate to `delivery.mode = "webhook"`.
### Model and thinking overrides
Isolated jobs (`agentTurn`) can override the model and thinking level:
@@ -213,11 +228,12 @@ Resolution priority:
Isolated jobs can deliver output to a channel via the top-level `delivery` config:
- `delivery.mode`: `announce` (deliver a summary) or `none`.
- `delivery.mode`: `announce` (channel delivery), `webhook` (HTTP POST), or `none`.
- `delivery.channel`: `whatsapp` / `telegram` / `discord` / `slack` / `mattermost` (plugin) / `signal` / `imessage` / `last`.
- `delivery.to`: channel-specific recipient target.
Delivery config is only valid for isolated jobs (`sessionTarget: "isolated"`).
`announce` delivery is only valid for isolated jobs (`sessionTarget: "isolated"`).
`webhook` delivery is valid for both main and isolated jobs.
If `delivery.channel` or `delivery.to` is omitted, cron can fall back to the main sessions
“last route” (the last place the agent replied).
@@ -333,10 +349,21 @@ Notes:
enabled: true, // default true
store: "~/.openclaw/cron/jobs.json",
maxConcurrentRuns: 1, // default 1
webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs
webhookToken: "replace-with-dedicated-webhook-token", // optional bearer token for webhook mode
},
}
```
Webhook behavior:
- Preferred: set `delivery.mode: "webhook"` with `delivery.to: "https://..."` per job.
- Webhook URLs must be valid `http://` or `https://` URLs.
- Payload is the cron finished event JSON.
- If `cron.webhookToken` is set, auth header is `Authorization: Bearer <cron.webhookToken>`.
- If `cron.webhookToken` is not set, no `Authorization` header is sent.
- Deprecated fallback: stored legacy jobs with `notify: true` still use `cron.webhook` when present.
Disable cron entirely:
- `cron.enabled: false` (config)

View File

@@ -87,6 +87,77 @@ Token resolution is account-aware. Config token values win over env fallback. `D
- Group DMs are ignored by default (`channels.discord.dm.groupEnabled=false`).
- Native slash commands run in isolated command sessions (`agent:<agentId>:discord:slash:<userId>`), while still carrying `CommandTargetSessionKey` to the routed conversation session.
## Interactive components
OpenClaw supports Discord components v2 containers for agent messages. Use the message tool with a `components` payload. Interaction results are routed back to the agent as normal inbound messages and follow the existing Discord `replyToMode` settings.
Supported blocks:
- `text`, `section`, `separator`, `actions`, `media-gallery`, `file`
- Action rows allow up to 5 buttons or a single select menu
- Select types: `string`, `user`, `role`, `mentionable`, `channel`
File attachments:
- `file` blocks must point to an attachment reference (`attachment://<filename>`)
- Provide the attachment via `media`/`path`/`filePath` (single file); use `media-gallery` for multiple files
- Use `filename` to override the upload name when it should match the attachment reference
Modal forms:
- Add `components.modal` with up to 5 fields
- Field types: `text`, `checkbox`, `radio`, `select`, `role-select`, `user-select`
- OpenClaw adds a trigger button automatically
Example:
```json5
{
channel: "discord",
action: "send",
to: "channel:123456789012345678",
message: "Optional fallback text",
components: {
text: "Choose a path",
blocks: [
{
type: "actions",
buttons: [
{ label: "Approve", style: "success" },
{ label: "Decline", style: "danger" },
],
},
{
type: "actions",
select: {
type: "string",
placeholder: "Pick an option",
options: [
{ label: "Option A", value: "a" },
{ label: "Option B", value: "b" },
],
},
},
],
modal: {
title: "Details",
triggerLabel: "Open form",
fields: [
{ type: "text", label: "Requester" },
{
type: "select",
label: "Priority",
options: [
{ label: "Low", value: "low" },
{ label: "High", value: "high" },
],
},
],
},
},
}
```
## Access control and routing
<Tabs>

View File

@@ -105,7 +105,7 @@ Want “groups can only see folder X” instead of “no host access”? Keep `w
docker: {
binds: [
// hostPath:containerPath:mode
"~/FriendsShared:/data:ro",
"/home/user/FriendsShared:/data:ro",
],
},
},

View File

@@ -176,12 +176,24 @@ Behavior:
## Sandbox Session Visibility
Sandboxed sessions can use session tools, but by default they only see sessions they spawned via `sessions_spawn`.
Session tools can be scoped to reduce cross-session access.
Default behavior:
- `tools.sessions.visibility` defaults to `tree` (current session + spawned subagent sessions).
- For sandboxed sessions, `agents.defaults.sandbox.sessionToolsVisibility` can hard-clamp visibility.
Config:
```json5
{
tools: {
sessions: {
// "self" | "tree" | "agent" | "all"
// default: "tree"
visibility: "tree",
},
},
agents: {
defaults: {
sandbox: {
@@ -192,3 +204,11 @@ Config:
},
}
```
Notes:
- `self`: only the current session key.
- `tree`: current session + sessions spawned by the current session.
- `agent`: any session belonging to the current agent id.
- `all`: any session (cross-agent access still requires `tools.agentToAgent`).
- When a session is sandboxed and `sessionToolsVisibility="spawned"`, OpenClaw clamps visibility to `tree` even if you set `tools.sessions.visibility="all"`.

View File

@@ -0,0 +1,192 @@
---
summary: "Production plan for reliable interactive process supervision (PTY + non-PTY) with explicit ownership, unified lifecycle, and deterministic cleanup"
owner: "openclaw"
status: "in-progress"
last_updated: "2026-02-15"
title: "PTY and Process Supervision Plan"
---
# PTY and Process Supervision Plan
## 1. Problem and goal
We need one reliable lifecycle for long-running command execution across:
- `exec` foreground runs
- `exec` background runs
- `process` follow up actions (`poll`, `log`, `send-keys`, `paste`, `submit`, `kill`, `remove`)
- CLI agent runner subprocesses
The goal is not just to support PTY. The goal is predictable ownership, cancellation, timeout, and cleanup with no unsafe process matching heuristics.
## 2. Scope and boundaries
- Keep implementation internal in `src/process/supervisor`.
- Do not create a new package for this.
- Keep current behavior compatibility where practical.
- Do not broaden scope to terminal replay or tmux style session persistence.
## 3. Implemented in this branch
### Supervisor baseline already present
- Supervisor module is in place under `src/process/supervisor/*`.
- Exec runtime and CLI runner are already routed through supervisor spawn and wait.
- Registry finalization is idempotent.
### This pass completed
1. Explicit PTY command contract
- `SpawnInput` is now a discriminated union in `src/process/supervisor/types.ts`.
- PTY runs require `ptyCommand` instead of reusing generic `argv`.
- Supervisor no longer rebuilds PTY command strings from argv joins in `src/process/supervisor/supervisor.ts`.
- Exec runtime now passes `ptyCommand` directly in `src/agents/bash-tools.exec-runtime.ts`.
2. Process layer type decoupling
- Supervisor types no longer import `SessionStdin` from agents.
- Process local stdin contract lives in `src/process/supervisor/types.ts` (`ManagedRunStdin`).
- Adapters now depend only on process level types:
- `src/process/supervisor/adapters/child.ts`
- `src/process/supervisor/adapters/pty.ts`
3. Process tool lifecycle ownership improvement
- `src/agents/bash-tools.process.ts` now requests cancellation through supervisor first.
- `process kill/remove` now use process-tree fallback termination when supervisor lookup misses.
- `remove` keeps deterministic remove behavior by dropping running session entries immediately after termination is requested.
4. Single source watchdog defaults
- Added shared defaults in `src/agents/cli-watchdog-defaults.ts`.
- `src/agents/cli-backends.ts` consumes the shared defaults.
- `src/agents/cli-runner/reliability.ts` consumes the same shared defaults.
5. Dead helper cleanup
- Removed unused `killSession` helper path from `src/agents/bash-tools.shared.ts`.
6. Direct supervisor path tests added
- Added `src/agents/bash-tools.process.supervisor.test.ts` to cover kill and remove routing through supervisor cancellation.
7. Reliability gap fixes completed
- `src/agents/bash-tools.process.ts` now falls back to real OS-level process termination when supervisor lookup misses.
- `src/process/supervisor/adapters/child.ts` now uses process-tree termination semantics for default cancel/timeout kill paths.
- Added shared process-tree utility in `src/process/kill-tree.ts`.
8. PTY contract edge-case coverage added
- Added `src/process/supervisor/supervisor.pty-command.test.ts` for verbatim PTY command forwarding and empty-command rejection.
- Added `src/process/supervisor/adapters/child.test.ts` for process-tree kill behavior in child adapter cancellation.
## 4. Remaining gaps and decisions
### Reliability status
The two required reliability gaps for this pass are now closed:
- `process kill/remove` now has a real OS termination fallback when supervisor lookup misses.
- child cancel/timeout now uses process-tree kill semantics for default kill path.
- Regression tests were added for both behaviors.
### Durability and startup reconciliation
Restart behavior is now explicitly defined as in-memory lifecycle only.
- `reconcileOrphans()` remains a no-op in `src/process/supervisor/supervisor.ts` by design.
- Active runs are not recovered after process restart.
- This boundary is intentional for this implementation pass to avoid partial persistence risks.
### Maintainability follow-ups
1. `runExecProcess` in `src/agents/bash-tools.exec-runtime.ts` still handles multiple responsibilities and can be split into focused helpers in a follow-up.
## 5. Implementation plan
The implementation pass for required reliability and contract items is complete.
Completed:
- `process kill/remove` fallback real termination
- process-tree cancellation for child adapter default kill path
- regression tests for fallback kill and child adapter kill path
- PTY command edge-case tests under explicit `ptyCommand`
- explicit in-memory restart boundary with `reconcileOrphans()` no-op by design
Optional follow-up:
- split `runExecProcess` into focused helpers with no behavior drift
## 6. File map
### Process supervisor
- `src/process/supervisor/types.ts` updated with discriminated spawn input and process local stdin contract.
- `src/process/supervisor/supervisor.ts` updated to use explicit `ptyCommand`.
- `src/process/supervisor/adapters/child.ts` and `src/process/supervisor/adapters/pty.ts` decoupled from agent types.
- `src/process/supervisor/registry.ts` idempotent finalize unchanged and retained.
### Exec and process integration
- `src/agents/bash-tools.exec-runtime.ts` updated to pass PTY command explicitly and keep fallback path.
- `src/agents/bash-tools.process.ts` updated to cancel via supervisor with real process-tree fallback termination.
- `src/agents/bash-tools.shared.ts` removed direct kill helper path.
### CLI reliability
- `src/agents/cli-watchdog-defaults.ts` added as shared baseline.
- `src/agents/cli-backends.ts` and `src/agents/cli-runner/reliability.ts` now consume same defaults.
## 7. Validation run in this pass
Unit tests:
- `pnpm vitest src/process/supervisor/registry.test.ts`
- `pnpm vitest src/process/supervisor/supervisor.test.ts`
- `pnpm vitest src/process/supervisor/supervisor.pty-command.test.ts`
- `pnpm vitest src/process/supervisor/adapters/child.test.ts`
- `pnpm vitest src/agents/cli-backends.test.ts`
- `pnpm vitest src/agents/bash-tools.exec.pty-cleanup.test.ts`
- `pnpm vitest src/agents/bash-tools.process.poll-timeout.test.ts`
- `pnpm vitest src/agents/bash-tools.process.supervisor.test.ts`
- `pnpm vitest src/process/exec.test.ts`
E2E targets:
- `pnpm test:e2e src/agents/cli-runner.e2e.test.ts`
- `pnpm test:e2e src/agents/bash-tools.exec.pty-fallback.e2e.test.ts src/agents/bash-tools.exec.background-abort.e2e.test.ts src/agents/bash-tools.process.send-keys.e2e.test.ts`
Typecheck note:
- `pnpm tsgo` currently fails in this repo due to a pre-existing UI typing dependency issue (`@vitest/browser-playwright` resolution), unrelated to this process supervision work.
## 8. Operational guarantees preserved
- Exec env hardening behavior is unchanged.
- Approval and allowlist flow is unchanged.
- Output sanitization and output caps are unchanged.
- PTY adapter still guarantees wait settlement on forced kill and listener disposal.
## 9. Definition of done
1. Supervisor is lifecycle owner for managed runs.
2. PTY spawn uses explicit command contract with no argv reconstruction.
3. Process layer has no type dependency on agent layer for supervisor stdin contracts.
4. Watchdog defaults are single source.
5. Targeted unit and e2e tests remain green.
6. Restart durability boundary is explicitly documented or fully implemented.
## 10. Summary
The branch now has a coherent and safer supervision shape:
- explicit PTY contract
- cleaner process layering
- supervisor driven cancellation path for process operations
- real fallback termination when supervisor lookup misses
- process-tree cancellation for child-run default kill paths
- unified watchdog defaults
- explicit in-memory restart boundary (no orphan reconciliation across restart in this pass)

View File

@@ -1508,6 +1508,31 @@ Provider auth follows standard order: auth profiles → env vars → `models.pro
}
```
### `tools.sessions`
Controls which sessions can be targeted by the session tools (`sessions_list`, `sessions_history`, `sessions_send`).
Default: `tree` (current session + sessions spawned by it, such as subagents).
```json5
{
tools: {
sessions: {
// "self" | "tree" | "agent" | "all"
visibility: "tree",
},
},
}
```
Notes:
- `self`: only the current session key.
- `tree`: current session + sessions spawned by the current session (subagents).
- `agent`: any session belonging to the current agent id (can include other users if you run per-sender sessions under the same agent id).
- `all`: any session. Cross-agent targeting still requires `tools.agentToAgent`.
- Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility="spawned"`, visibility is forced to `tree` even if `tools.sessions.visibility="all"`.
### `tools.subagents`
```json5
@@ -2295,12 +2320,16 @@ Current builds no longer include the TCP bridge. Nodes connect over the Gateway
cron: {
enabled: true,
maxConcurrentRuns: 2,
webhook: "https://example.invalid/legacy", // deprecated fallback for stored notify:true jobs
webhookToken: "replace-with-dedicated-token", // optional bearer token for outbound webhook auth
sessionRetention: "24h", // duration string or false
},
}
```
- `sessionRetention`: how long to keep completed cron sessions before pruning. Default: `24h`.
- `webhookToken`: bearer token used for cron webhook POST delivery (`delivery.mode = "webhook"`), if omitted no auth header is sent.
- `webhook`: deprecated legacy fallback webhook URL (http/https) used only for stored jobs that still have `notify: true`.
See [Cron Jobs](/automation/cron-jobs).

View File

@@ -76,7 +76,7 @@ Global and per-agent binds are **merged** (not replaced). Under `scope: "shared"
- When set (including `[]`), it replaces `agents.defaults.sandbox.docker.binds` for the browser container.
- When omitted, the browser container falls back to `agents.defaults.sandbox.docker.binds` (backwards compatible).
Example (read-only source + docker socket):
Example (read-only source + an extra data directory):
```json5
{
@@ -84,7 +84,7 @@ Example (read-only source + docker socket):
defaults: {
sandbox: {
docker: {
binds: ["/home/user/source:/source:ro", "/var/run/docker.sock:/var/run/docker.sock"],
binds: ["/home/user/source:/source:ro", "/var/data/myapp:/data:ro"],
},
},
},
@@ -105,7 +105,8 @@ Example (read-only source + docker socket):
Security notes:
- Binds bypass the sandbox filesystem: they expose host paths with whatever mode you set (`:ro` or `:rw`).
- Sensitive mounts (e.g., `docker.sock`, secrets, SSH keys) should be `:ro` unless absolutely required.
- OpenClaw blocks dangerous bind sources (for example: `docker.sock`, `/etc`, `/proc`, `/sys`, `/dev`, and parent mounts that would expose them).
- Sensitive mounts (secrets, SSH keys, service credentials) should be `:ro` unless absolutely required.
- Combine with `workspaceAccess: "ro"` if you only need read access to the workspace; bind modes stay independent.
- See [Sandbox vs Tool Policy vs Elevated](/gateway/sandbox-vs-tool-policy-vs-elevated) for how binds interact with tool policy and elevated exec.

View File

@@ -710,7 +710,11 @@ Common use cases:
scope: "agent",
workspaceAccess: "none",
},
// Session tools can reveal sensitive data from transcripts. By default OpenClaw limits these tools
// to the current session + spawned subagent sessions, but you can clamp further if needed.
// See `tools.sessions.visibility` in the configuration reference.
tools: {
sessions: { visibility: "tree" }, // self | tree | agent | all
allow: [
"sessions_list",
"sessions_history",

View File

@@ -34,17 +34,17 @@ Notes:
# From repo root; set release IDs so Sparkle feed is enabled.
# APP_BUILD must be numeric + monotonic for Sparkle compare.
BUNDLE_ID=bot.molt.mac \
APP_VERSION=2026.2.15 \
APP_VERSION=2026.2.16 \
APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
scripts/package-mac-app.sh
# Zip for distribution (includes resource forks for Sparkle delta support)
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.2.15.zip
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.2.16.zip
# Optional: also build a styled DMG for humans (drag to /Applications)
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.2.15.dmg
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.2.16.dmg
# Recommended: build + notarize/staple zip + DMG
# First, create a keychain profile once:
@@ -52,14 +52,14 @@ scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.2.15.dmg
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
NOTARIZE=1 NOTARYTOOL_PROFILE=openclaw-notary \
BUNDLE_ID=bot.molt.mac \
APP_VERSION=2026.2.15 \
APP_VERSION=2026.2.16 \
APP_BUILD="$(git rev-list --count HEAD)" \
BUILD_CONFIG=release \
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
scripts/package-mac-dist.sh
# Optional: ship dSYM alongside the release
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.2.15.dSYM.zip
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.2.16.dSYM.zip
```
## Appcast entry
@@ -67,7 +67,7 @@ ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenCl
Use the release note generator so Sparkle renders formatted HTML notes:
```bash
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.2.15.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.2.16.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
```
Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry.
@@ -75,7 +75,7 @@ Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when
## Publish & verify
- Upload `OpenClaw-2026.2.15.zip` (and `OpenClaw-2026.2.15.dSYM.zip`) to the GitHub release for tag `v2026.2.15`.
- Upload `OpenClaw-2026.2.16.zip` (and `OpenClaw-2026.2.16.dSYM.zip`) to the GitHub release for tag `v2026.2.16`.
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml`.
- Sanity checks:
- `curl -I https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml` returns 200.

View File

@@ -442,12 +442,14 @@ Notes:
- `main` is the canonical direct-chat key; global/unknown are hidden.
- `messageLimit > 0` fetches last N messages per session (tool messages filtered).
- Session targeting is controlled by `tools.sessions.visibility` (default `tree`: current session + spawned subagent sessions). If you run a shared agent for multiple users, consider setting `tools.sessions.visibility: "self"` to prevent cross-session browsing.
- `sessions_send` waits for final completion when `timeoutSeconds > 0`.
- Delivery/announce happens after completion and is best-effort; `status: "ok"` confirms the agent run finished, not that the announce was delivered.
- `sessions_spawn` starts a sub-agent run and posts an announce reply back to the requester chat.
- `sessions_spawn` is non-blocking and returns `status: "accepted"` immediately.
- `sessions_send` runs a replyback pingpong (reply `REPLY_SKIP` to stop; max turns via `session.agentToAgent.maxPingPongTurns`, 05).
- After the pingpong, the target agent runs an **announce step**; reply `ANNOUNCE_SKIP` to suppress the announcement.
- Sandbox clamp: when the current session is sandboxed and `agents.defaults.sandbox.sessionToolsVisibility: "spawned"`, OpenClaw clamps `tools.sessions.visibility` to `tree`.
### `agents_list`

View File

@@ -324,6 +324,7 @@ Legacy `agent.*` configs are migrated by `openclaw doctor`; prefer `agents.defau
```json
{
"tools": {
"sessions": { "visibility": "tree" },
"allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
"deny": ["exec", "write", "edit", "apply_patch", "read", "browser"]
}

View File

@@ -224,6 +224,7 @@ Fetch a URL and extract readable content.
enabled: true,
maxChars: 50000,
maxCharsCap: 50000,
maxResponseBytes: 2000000,
timeoutSeconds: 30,
cacheTtlMinutes: 15,
maxRedirects: 3,
@@ -256,6 +257,7 @@ Notes:
- `web_fetch` sends a Chrome-like User-Agent and `Accept-Language` by default; override `userAgent` if needed.
- `web_fetch` blocks private/internal hostnames and re-checks redirects (limit with `maxRedirects`).
- `maxChars` is clamped to `tools.web.fetch.maxCharsCap`.
- `web_fetch` caps the downloaded response body size to `tools.web.fetch.maxResponseBytes` before parsing; oversized responses are truncated and include a warning.
- `web_fetch` is best-effort extraction; some sites will need the browser tool.
- See [Firecrawl](/tools/firecrawl) for key setup and service details.
- Responses are cached (default 15 minutes) to reduce repeated fetches.

View File

@@ -83,6 +83,10 @@ Cron jobs panel notes:
- For isolated jobs, delivery defaults to announce summary. You can switch to none if you want internal-only runs.
- Channel/target fields appear when announce is selected.
- Webhook mode uses `delivery.mode = "webhook"` with `delivery.to` set to a valid HTTP(S) webhook URL.
- For main-session jobs, webhook and none delivery modes are available.
- Set `cron.webhookToken` to send a dedicated bearer token, if omitted the webhook is sent without an auth header.
- Deprecated fallback: stored legacy jobs with `notify: true` can still use `cron.webhook` until migrated.
## Chat behavior
@@ -93,6 +97,10 @@ Cron jobs panel notes:
- Click **Stop** (calls `chat.abort`)
- Type `/stop` (or `stop|esc|abort|wait|exit|interrupt`) to abort out-of-band
- `chat.abort` supports `{ sessionKey }` (no `runId`) to abort all active runs for that session
- Abort partial retention:
- When a run is aborted, partial assistant text can still be shown in the UI
- Gateway persists aborted partial assistant text into transcript history when buffered output exists
- Persisted entries include abort metadata so transcript consumers can tell abort partials from normal completion output
## Tailnet access (recommended)

View File

@@ -25,6 +25,8 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket.
- The UI connects to the Gateway WebSocket and uses `chat.history`, `chat.send`, and `chat.inject`.
- `chat.inject` appends an assistant note directly to the transcript and broadcasts it to the UI (no agent run).
- Aborted runs can keep partial assistant output visible in the UI.
- Gateway persists aborted partial assistant text into transcript history when buffered output exists, and marks those entries with abort metadata.
- History is always fetched from the gateway (no local file watching).
- If the gateway is unreachable, WebChat is read-only.

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/bluebubbles",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw BlueBubbles channel plugin",
"type": "module",
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/copilot-proxy",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Copilot Proxy provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/diagnostics-otel",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw diagnostics OpenTelemetry exporter",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/discord",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw Discord channel plugin",
"type": "module",
"devDependencies": {

View File

@@ -158,6 +158,12 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
threading: {
resolveReplyToMode: ({ cfg }) => cfg.channels?.discord?.replyToMode ?? "off",
},
agentPrompt: {
messageToolHints: () => [
"- Discord components: set `components` when sending messages to include buttons, selects, or v2 containers.",
"- Forms: add `components.modal` (title, fields). OpenClaw adds a trigger button and routes submissions as new messages.",
],
},
messaging: {
normalizeTarget: normalizeDiscordMessagingTarget,
targetResolver: {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/feishu",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/google-antigravity-auth",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Google Antigravity OAuth provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/google-gemini-cli-auth",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Gemini CLI OAuth provider plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/googlechat",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Google Chat channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/imessage",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw iMessage channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/irc",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw IRC channel plugin",
"type": "module",
"devDependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/line",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw LINE channel plugin",
"type": "module",

View File

@@ -0,0 +1,101 @@
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
import { describe, expect, it, vi } from "vitest";
import { linePlugin } from "./channel.js";
import { setLineRuntime } from "./runtime.js";
function createRuntime() {
const probeLineBot = vi.fn(async () => ({ ok: false }));
const monitorLineProvider = vi.fn(async () => ({
account: { accountId: "default" },
handleWebhook: async () => {},
stop: () => {},
}));
const runtime = {
channel: {
line: {
probeLineBot,
monitorLineProvider,
},
},
logging: {
shouldLogVerbose: () => false,
},
} as unknown as PluginRuntime;
return { runtime, probeLineBot, monitorLineProvider };
}
function createStartAccountCtx(params: { token: string; secret: string; runtime: unknown }) {
return {
account: {
accountId: "default",
channelAccessToken: params.token,
channelSecret: params.secret,
config: {},
},
cfg: {} as OpenClawConfig,
runtime: params.runtime,
abortSignal: undefined,
log: { info: vi.fn(), debug: vi.fn() },
};
}
describe("linePlugin gateway.startAccount", () => {
it("fails startup when channel secret is missing", async () => {
const { runtime, monitorLineProvider } = createRuntime();
setLineRuntime(runtime);
await expect(
linePlugin.gateway.startAccount(
createStartAccountCtx({
token: "token",
secret: " ",
runtime: {},
}) as never,
),
).rejects.toThrow(
'LINE webhook mode requires a non-empty channel secret for account "default".',
);
expect(monitorLineProvider).not.toHaveBeenCalled();
});
it("fails startup when channel access token is missing", async () => {
const { runtime, monitorLineProvider } = createRuntime();
setLineRuntime(runtime);
await expect(
linePlugin.gateway.startAccount(
createStartAccountCtx({
token: " ",
secret: "secret",
runtime: {},
}) as never,
),
).rejects.toThrow(
'LINE webhook mode requires a non-empty channel access token for account "default".',
);
expect(monitorLineProvider).not.toHaveBeenCalled();
});
it("starts provider when token and secret are present", async () => {
const { runtime, monitorLineProvider } = createRuntime();
setLineRuntime(runtime);
await linePlugin.gateway.startAccount(
createStartAccountCtx({
token: "token",
secret: "secret",
runtime: {},
}) as never,
);
expect(monitorLineProvider).toHaveBeenCalledWith(
expect.objectContaining({
channelAccessToken: "token",
channelSecret: "secret",
accountId: "default",
}),
);
});
});

View File

@@ -119,12 +119,13 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
},
};
},
isConfigured: (account) => Boolean(account.channelAccessToken?.trim()),
isConfigured: (account) =>
Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.channelAccessToken?.trim()),
configured: Boolean(account.channelAccessToken?.trim() && account.channelSecret?.trim()),
tokenSource: account.tokenSource ?? undefined,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
@@ -603,7 +604,9 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
probeAccount: async ({ account, timeoutMs }) =>
getLineRuntime().channel.line.probeLineBot(account.channelAccessToken, timeoutMs),
buildAccountSnapshot: ({ account, runtime, probe }) => {
const configured = Boolean(account.channelAccessToken?.trim());
const configured = Boolean(
account.channelAccessToken?.trim() && account.channelSecret?.trim(),
);
return {
accountId: account.accountId,
name: account.name,
@@ -626,6 +629,16 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = {
const account = ctx.account;
const token = account.channelAccessToken.trim();
const secret = account.channelSecret.trim();
if (!token) {
throw new Error(
`LINE webhook mode requires a non-empty channel access token for account "${account.accountId}".`,
);
}
if (!secret) {
throw new Error(
`LINE webhook mode requires a non-empty channel secret for account "${account.accountId}".`,
);
}
let lineBotLabel = "";
try {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/llm-task",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw JSON-only LLM task plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/lobster",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)",
"type": "module",
"devDependencies": {

View File

@@ -1,5 +1,11 @@
# Changelog
## 2026.2.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.2.15
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/matrix",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw Matrix channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/mattermost",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Mattermost channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-core",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw core memory search plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/memory-lancedb",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw LanceDB-backed long-term memory plugin with auto-recall/capture",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/minimax-portal-auth",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw MiniMax Portal OAuth provider plugin",
"type": "module",

View File

@@ -1,5 +1,11 @@
# Changelog
## 2026.2.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.2.15
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/msteams",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw Microsoft Teams channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nextcloud-talk",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw Nextcloud Talk channel plugin",
"type": "module",
"devDependencies": {

View File

@@ -1,5 +1,11 @@
# Changelog
## 2026.2.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.2.15
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/nostr",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw Nostr channel plugin for NIP-04 encrypted DMs",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/open-prose",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenProse VM skill pack plugin (slash command + telemetry).",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/signal",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Signal channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/slack",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Slack channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/telegram",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Telegram channel plugin",
"type": "module",

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/tlon",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Tlon/Urbit channel plugin",
"type": "module",

View File

@@ -1,5 +1,11 @@
# Changelog
## 2026.2.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.2.15
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/twitch",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw Twitch channel plugin",
"type": "module",

View File

@@ -1,5 +1,11 @@
# Changelog
## 2026.2.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.2.15
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/voice-call",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw voice-call plugin",
"type": "module",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/whatsapp",
"version": "2026.2.15",
"version": "2026.2.16",
"private": true,
"description": "OpenClaw WhatsApp channel plugin",
"type": "module",

View File

@@ -1,5 +1,11 @@
# Changelog
## 2026.2.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.2.15
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/zalo",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw Zalo channel plugin",
"type": "module",
"dependencies": {

View File

@@ -1,5 +1,11 @@
# Changelog
## 2026.2.16
### Changes
- Version alignment with core OpenClaw release numbers.
## 2026.2.15
### Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@openclaw/zalouser",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "OpenClaw Zalo Personal Account plugin via zca-cli",
"type": "module",
"dependencies": {

View File

@@ -1,9 +1,38 @@
#!/bin/sh
FILES=$(git diff --cached --name-only --diff-filter=ACMR | sed 's| |\\ |g')
[ -z "$FILES" ] && exit 0
#!/usr/bin/env bash
echo "$FILES" | xargs pnpm lint --fix
echo "$FILES" | xargs pnpm format --no-error-on-unmatched-pattern
echo "$FILES" | xargs git add
set -euo pipefail
exit 0
ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
RUN_NODE_TOOL="$ROOT_DIR/scripts/pre-commit/run-node-tool.sh"
FILTER_FILES="$ROOT_DIR/scripts/pre-commit/filter-staged-files.mjs"
if [[ ! -x "$RUN_NODE_TOOL" ]]; then
echo "Missing helper: $RUN_NODE_TOOL" >&2
exit 1
fi
if [[ ! -f "$FILTER_FILES" ]]; then
echo "Missing helper: $FILTER_FILES" >&2
exit 1
fi
# Security: avoid option-injection from malicious file names (e.g. "--all", "--force").
# Robustness: NUL-delimited file list handles spaces/newlines safely.
mapfile -d '' -t files < <(git diff --cached --name-only --diff-filter=ACMR -z)
if [ "${#files[@]}" -eq 0 ]; then
exit 0
fi
mapfile -d '' -t lint_files < <(node "$FILTER_FILES" lint -- "${files[@]}")
mapfile -d '' -t format_files < <(node "$FILTER_FILES" format -- "${files[@]}")
if [ "${#lint_files[@]}" -gt 0 ]; then
"$RUN_NODE_TOOL" oxlint --type-aware --fix -- "${lint_files[@]}"
fi
if [ "${#format_files[@]}" -gt 0 ]; then
"$RUN_NODE_TOOL" oxfmt --write -- "${format_files[@]}"
fi
git add -- "${files[@]}"

View File

@@ -1,13 +1,25 @@
{
"name": "openclaw",
"version": "2026.2.15",
"version": "2026.2.16",
"description": "Multi-channel AI gateway with extensible messaging integrations",
"keywords": [],
"homepage": "https://github.com/openclaw/openclaw#readme",
"bugs": {
"url": "https://github.com/openclaw/openclaw/issues"
},
"license": "MIT",
"author": "",
"repository": {
"type": "git",
"url": "git+https://github.com/openclaw/openclaw.git"
},
"bin": {
"openclaw": "openclaw.mjs"
},
"directories": {
"doc": "docs",
"test": "test"
},
"files": [
"CHANGELOG.md",
"LICENSE",
@@ -160,7 +172,7 @@
"sharp": "^0.34.5",
"signal-utils": "^0.21.1",
"sqlite-vec": "0.1.7-alpha.2",
"tar": "7.5.7",
"tar": "7.5.9",
"tslog": "^4.10.2",
"undici": "^7.22.0",
"ws": "^8.19.0",
@@ -177,13 +189,13 @@
"@types/proper-lockfile": "^4.1.4",
"@types/qrcode-terminal": "^0.12.2",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "7.0.0-dev.20260214.1",
"@typescript/native-preview": "7.0.0-dev.20260215.1",
"@vitest/coverage-v8": "^4.0.18",
"lit": "^3.3.2",
"ollama": "^0.6.3",
"oxfmt": "0.32.0",
"oxlint": "^1.47.0",
"oxlint-tsgolint": "^0.12.2",
"oxlint-tsgolint": "^0.13.0",
"rolldown": "1.0.0-rc.4",
"tsdown": "^0.20.3",
"tsx": "^4.21.0",
@@ -205,7 +217,7 @@
"form-data": "2.5.4",
"qs": "6.14.2",
"@sinclair/typebox": "0.34.48",
"tar": "7.5.7",
"tar": "7.5.9",
"tough-cookie": "4.1.3"
},
"onlyBuiltDependencies": [

162
pnpm-lock.yaml generated
View File

@@ -9,7 +9,7 @@ overrides:
form-data: 2.5.4
qs: 6.14.2
'@sinclair/typebox': 0.34.48
tar: 7.5.7
tar: 7.5.9
tough-cookie: 4.1.3
importers:
@@ -161,8 +161,8 @@ importers:
specifier: 0.1.7-alpha.2
version: 0.1.7-alpha.2
tar:
specifier: 7.5.7
version: 7.5.7
specifier: 7.5.9
version: 7.5.9
tslog:
specifier: ^4.10.2
version: 4.10.2
@@ -207,8 +207,8 @@ importers:
specifier: ^8.18.1
version: 8.18.1
'@typescript/native-preview':
specifier: 7.0.0-dev.20260214.1
version: 7.0.0-dev.20260214.1
specifier: 7.0.0-dev.20260215.1
version: 7.0.0-dev.20260215.1
'@vitest/coverage-v8':
specifier: ^4.0.18
version: 4.0.18(@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18))(vitest@4.0.18)
@@ -223,16 +223,16 @@ importers:
version: 0.32.0
oxlint:
specifier: ^1.47.0
version: 1.47.0(oxlint-tsgolint@0.12.2)
version: 1.47.0(oxlint-tsgolint@0.13.0)
oxlint-tsgolint:
specifier: ^0.12.2
version: 0.12.2
specifier: ^0.13.0
version: 0.13.0
rolldown:
specifier: 1.0.0-rc.4
version: 1.0.0-rc.4
tsdown:
specifier: ^0.20.3
version: 0.20.3(@typescript/native-preview@7.0.0-dev.20260214.1)(typescript@5.9.3)
version: 0.20.3(@typescript/native-preview@7.0.0-dev.20260215.1)(typescript@5.9.3)
tsx:
specifier: ^4.21.0
version: 4.21.0
@@ -783,8 +783,8 @@ packages:
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
engines: {node: '>=6.9.0'}
'@babel/helper-string-parser@8.0.0-rc.1':
resolution: {integrity: sha512-vi/pfmbrOtQmqgfboaBhaCU50G7mcySVu69VU8z+lYoPPB6WzI9VgV7WQfL908M4oeSH5fDkmoupIqoE0SdApw==}
'@babel/helper-string-parser@8.0.0-rc.2':
resolution: {integrity: sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ==}
engines: {node: ^20.19.0 || >=22.12.0}
'@babel/helper-validator-identifier@7.28.5':
@@ -2059,33 +2059,33 @@ packages:
cpu: [x64]
os: [win32]
'@oxlint-tsgolint/darwin-arm64@0.12.2':
resolution: {integrity: sha512-XIfavTqkJPGYi/98z7ZCkZvXq2AccMAAB0iwvKDRTQqiweMXVUyeUdx46phCHHH1PgmIVJtVfysThkHq2xCyrw==}
'@oxlint-tsgolint/darwin-arm64@0.13.0':
resolution: {integrity: sha512-OWQ3U+oDjjupmX0WU9oYyKF2iUOKDMLW/+zan0cd0vYIGId80xTRHHA8oXnREmK8dsMMP3nV3VXME3NH/hS0lw==}
cpu: [arm64]
os: [darwin]
'@oxlint-tsgolint/darwin-x64@0.12.2':
resolution: {integrity: sha512-tytsvP6zmNShRNDo4GgQartOXmd4GPd+TylCUMdO/iWl9PZVOgRyswWbYVTNgn85Cib/aY2q3Uu+jOw+QlbxvQ==}
'@oxlint-tsgolint/darwin-x64@0.13.0':
resolution: {integrity: sha512-wZvgj+eVqNkCUjSq2ExlMdbGDpZfaw6J+YctQV1pkGFdn7Y9cySWdfwu5v/AW2JPsJbFMXJ8GAr+WoZbRapz2A==}
cpu: [x64]
os: [darwin]
'@oxlint-tsgolint/linux-arm64@0.12.2':
resolution: {integrity: sha512-3W38yJuF7taEquhEuD6mYQyCeWNAlc1pNPjFkspkhLKZVgbrhDA4V6fCxLDDRvrTHde0bXPmFvuPlUq5pSePgA==}
'@oxlint-tsgolint/linux-arm64@0.13.0':
resolution: {integrity: sha512-nwtf5BgHbAWSVwyIF00l6QpfyFcpDMp6D+3cpe6NTgBYMSSSC0Ip1gswUwzVccOPoQK48t+J6vHyURQ96M1KDg==}
cpu: [arm64]
os: [linux]
'@oxlint-tsgolint/linux-x64@0.12.2':
resolution: {integrity: sha512-EjcEspeeV0NmaopEp4wcN5ntQP9VCJJDrTvzOjMP4W6ajz18M+pni9vkKvmcPIpRa/UmWobeFgKoVd/KGueeuQ==}
'@oxlint-tsgolint/linux-x64@0.13.0':
resolution: {integrity: sha512-Rkzgj38eVoGSBuGDaCrALS4FM19+m1Qlv0hjB4MWvXUej014XkB5ze+svYE3HX+AAm1ey9QYj/CQzfz203FPIg==}
cpu: [x64]
os: [linux]
'@oxlint-tsgolint/win32-arm64@0.12.2':
resolution: {integrity: sha512-a9L7iA5K/Ht/i8d9+7RTp6hbPa4cyXP0MdySVXAO6vczpL/4ildfY9Hr2m2wqL12uK6xe/uVABpVTrqay/wV+g==}
'@oxlint-tsgolint/win32-arm64@0.13.0':
resolution: {integrity: sha512-Y+0hFqLT5M7UIvGvTR3QFK27l17FqXk6UwwpBFOcyBGJ5bLd1RaAPWjqTmcgPvdolA6FCMeW1pxZuNtKDlYd7A==}
cpu: [arm64]
os: [win32]
'@oxlint-tsgolint/win32-x64@0.12.2':
resolution: {integrity: sha512-Cvt40UbTf5ib12DjGN+mMGOnjWa4Bc6Y7KEaXXp9qzckvs3HpNk2wSwMV3gnuR8Ipx4hkzkzrgzD0BAUsySAfA==}
'@oxlint-tsgolint/win32-x64@0.13.0':
resolution: {integrity: sha512-mXjTttzyyfl8d/XvxggmZFBq0pbQmRvHbjQEv70YECNaLEHG8j8WYUwLa641uudAnV1VoBI34pc7bmgJM7qhOA==}
cpu: [x64]
os: [win32]
@@ -2995,43 +2995,43 @@ packages:
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260214.1':
resolution: {integrity: sha512-Jb2WcLGpTOC6x58e8QPYC/14xmDbnbFIuKqUvYoI77hVtojVyxZi8L5Y4CgYqXYx8vRWmIFk35c1OGdtPip6Sg==}
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260215.1':
resolution: {integrity: sha512-icVO/hEMXjWlKhmpjIpqDyCzPvtHqfrPB+2rkd6M3rz84Bmw+o8Xgd7JvRxryZhR+D0y55me/bKh9xgvsgzuhA==}
cpu: [arm64]
os: [darwin]
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260214.1':
resolution: {integrity: sha512-O9l2gVuQFZsb8NIQtu0HN5Tn/Hw2fwylPOPS/0Y4oW+FUMhkqtvetUkb3zZ0qj7capilZ4YnmyGYg3TDqkP4Nw==}
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260215.1':
resolution: {integrity: sha512-Wz73wf1o9+4KwCLg8wnnIZZDAvv2KRZlDyP4X8GfBNzajfIAwYvI0ANWuIDznUUGeDAcqhBJXNe0Bkf4H9y4mg==}
cpu: [x64]
os: [darwin]
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260214.1':
resolution: {integrity: sha512-Hl4e3yxJqzIGgFI8aH/rLGW+a7kSLHJCpAd5JOLG7hHKnamZF4SjlunnoHLV4IcMri+G6UE3W/84i0QvQP5wLA==}
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260215.1':
resolution: {integrity: sha512-AYyXRxVwLZzfkEYN8FGdV4vqXwbTmv93nAZ6gMLvpDG4ItOybAE1R2obFjlFc+Or/rfQmVvfdkTym3c4bRJ3XQ==}
cpu: [arm64]
os: [linux]
'@typescript/native-preview-linux-arm@7.0.0-dev.20260214.1':
resolution: {integrity: sha512-TaFrVnx3iXtl/oH1hzwvFyqWj9tzkjW8Ufl2m0Vx2/7GXnzZadm2KA6tFpGbzzWbZJznmXxKHL4O3AZRQYyZqQ==}
'@typescript/native-preview-linux-arm@7.0.0-dev.20260215.1':
resolution: {integrity: sha512-6WVXFVSp3LBBiBgBMtAHQgTDN72mDhgjrmXH7GoABTxR9asK8oPfmy5cwTp1sPD46pYhqjnSHMrARyg2FaNSeA==}
cpu: [arm]
os: [linux]
'@typescript/native-preview-linux-x64@7.0.0-dev.20260214.1':
resolution: {integrity: sha512-a/JypIXTc/tdodhYdQm24WH6aTfnJJjDbwxce4BS2g6IzYSc2GFcZBvlq1CJYS2FAVLpiSxj0OFAZmgjpCDAKg==}
'@typescript/native-preview-linux-x64@7.0.0-dev.20260215.1':
resolution: {integrity: sha512-Ui6qbTO+nE7fwh5OGTGfL4ndaT+SpiUiv0F1m3+nMaiAKysY5GbgXUfzWzkSrOODsT8F/4jZ4wCzEzJordt8sQ==}
cpu: [x64]
os: [linux]
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260214.1':
resolution: {integrity: sha512-MJGPEDvdXj8olcWH0P+cWYcaN4r/0J4aSbcaISlen3MZ/2hrrgNl46PV4eGJKKCDniY2pH2fJzrMyJWZOcdb0w==}
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260215.1':
resolution: {integrity: sha512-dBFyAH9h3bMUaIp/84c3gKwyQ6jQmtzVoIBamSrYNw0xinJ56A/Ln5igdNOYrH8+/Aofmeh7pAWaa8U456XMjw==}
cpu: [arm64]
os: [win32]
'@typescript/native-preview-win32-x64@7.0.0-dev.20260214.1':
resolution: {integrity: sha512-BtF48TRUyiCKznlOcQ7r7EXhonGSanm9X2eu7d8Yq1vaWO5SDgB0e+ISQXSoIfs3a1S3d5S5QV/vTE4+vocPxA==}
'@typescript/native-preview-win32-x64@7.0.0-dev.20260215.1':
resolution: {integrity: sha512-bEMSwX71OGGvfsfHEa/aX7ZUWbPSI2oKEmeWcDQVY8vH1VK1ZwcFzMhKfgVJPt5pKH2bK3EO3xYnAyKkDO/Ung==}
cpu: [x64]
os: [win32]
'@typescript/native-preview@7.0.0-dev.20260214.1':
resolution: {integrity: sha512-BDM0ZLf2v6ilR0tDi8OMEr4X08lFCToPk3/p1SSE4GhagzmlU/5b+9slR0kKtaKMrds01FhvaKx6U9+NmAWgbQ==}
'@typescript/native-preview@7.0.0-dev.20260215.1':
resolution: {integrity: sha512-grs0BbJyPR7VLNerBVteEToPku1InMKVKVKBUTJi19LfK+LU3+pkU6/fsTfZhH3xmIzIxD/sNRQHLt4x/Yb9yg==}
hasBin: true
'@typespec/ts-http-runtime@0.3.3':
@@ -4728,8 +4728,8 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
oxlint-tsgolint@0.12.2:
resolution: {integrity: sha512-IFiOhYZfSgiHbBznTZOhFpEHpsZFSP0j7fVRake03HEkgH0YljnTFDNoRkGWsTrnrHr7nRIomSsF4TnCI/O+kQ==}
oxlint-tsgolint@0.13.0:
resolution: {integrity: sha512-VUOWP5T9R9RwuPLKvNgvhsjdPFVhr2k8no8ea84+KhDtYPmk9L/3StNP3WClyPOKJOT8bFlO3eyhTKxXK9+Oog==}
hasBin: true
oxlint@1.47.0:
@@ -5364,8 +5364,8 @@ packages:
resolution: {integrity: sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==}
engines: {node: '>=12.17'}
tar@7.5.7:
resolution: {integrity: sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==}
tar@7.5.9:
resolution: {integrity: sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==}
engines: {node: '>=18'}
thenify-all@1.6.0:
@@ -6285,7 +6285,7 @@ snapshots:
'@babel/helper-string-parser@7.27.1': {}
'@babel/helper-string-parser@8.0.0-rc.1': {}
'@babel/helper-string-parser@8.0.0-rc.2': {}
'@babel/helper-validator-identifier@7.28.5': {}
@@ -6308,7 +6308,7 @@ snapshots:
'@babel/types@8.0.0-rc.1':
dependencies:
'@babel/helper-string-parser': 8.0.0-rc.1
'@babel/helper-string-parser': 8.0.0-rc.2
'@babel/helper-validator-identifier': 8.0.0-rc.1
'@bcoe/v8-coverage@1.0.2': {}
@@ -7546,22 +7546,22 @@ snapshots:
'@oxfmt/binding-win32-x64-msvc@0.32.0':
optional: true
'@oxlint-tsgolint/darwin-arm64@0.12.2':
'@oxlint-tsgolint/darwin-arm64@0.13.0':
optional: true
'@oxlint-tsgolint/darwin-x64@0.12.2':
'@oxlint-tsgolint/darwin-x64@0.13.0':
optional: true
'@oxlint-tsgolint/linux-arm64@0.12.2':
'@oxlint-tsgolint/linux-arm64@0.13.0':
optional: true
'@oxlint-tsgolint/linux-x64@0.12.2':
'@oxlint-tsgolint/linux-x64@0.13.0':
optional: true
'@oxlint-tsgolint/win32-arm64@0.12.2':
'@oxlint-tsgolint/win32-arm64@0.13.0':
optional: true
'@oxlint-tsgolint/win32-x64@0.12.2':
'@oxlint-tsgolint/win32-x64@0.13.0':
optional: true
'@oxlint/binding-android-arm-eabi@1.47.0':
@@ -8473,36 +8473,36 @@ snapshots:
dependencies:
'@types/node': 25.2.3
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260214.1':
'@typescript/native-preview-darwin-arm64@7.0.0-dev.20260215.1':
optional: true
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260214.1':
'@typescript/native-preview-darwin-x64@7.0.0-dev.20260215.1':
optional: true
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260214.1':
'@typescript/native-preview-linux-arm64@7.0.0-dev.20260215.1':
optional: true
'@typescript/native-preview-linux-arm@7.0.0-dev.20260214.1':
'@typescript/native-preview-linux-arm@7.0.0-dev.20260215.1':
optional: true
'@typescript/native-preview-linux-x64@7.0.0-dev.20260214.1':
'@typescript/native-preview-linux-x64@7.0.0-dev.20260215.1':
optional: true
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260214.1':
'@typescript/native-preview-win32-arm64@7.0.0-dev.20260215.1':
optional: true
'@typescript/native-preview-win32-x64@7.0.0-dev.20260214.1':
'@typescript/native-preview-win32-x64@7.0.0-dev.20260215.1':
optional: true
'@typescript/native-preview@7.0.0-dev.20260214.1':
'@typescript/native-preview@7.0.0-dev.20260215.1':
optionalDependencies:
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260214.1
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260214.1
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260214.1
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260214.1
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260214.1
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260214.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260214.1
'@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260215.1
'@typescript/native-preview-darwin-x64': 7.0.0-dev.20260215.1
'@typescript/native-preview-linux-arm': 7.0.0-dev.20260215.1
'@typescript/native-preview-linux-arm64': 7.0.0-dev.20260215.1
'@typescript/native-preview-linux-x64': 7.0.0-dev.20260215.1
'@typescript/native-preview-win32-arm64': 7.0.0-dev.20260215.1
'@typescript/native-preview-win32-x64': 7.0.0-dev.20260215.1
'@typespec/ts-http-runtime@0.3.3':
dependencies:
@@ -8990,7 +8990,7 @@ snapshots:
npmlog: 6.0.2
rc: 1.2.8
semver: 7.7.4
tar: 7.5.7
tar: 7.5.9
url-join: 4.0.1
which: 2.0.2
yargs: 17.7.2
@@ -10385,16 +10385,16 @@ snapshots:
'@oxfmt/binding-win32-ia32-msvc': 0.32.0
'@oxfmt/binding-win32-x64-msvc': 0.32.0
oxlint-tsgolint@0.12.2:
oxlint-tsgolint@0.13.0:
optionalDependencies:
'@oxlint-tsgolint/darwin-arm64': 0.12.2
'@oxlint-tsgolint/darwin-x64': 0.12.2
'@oxlint-tsgolint/linux-arm64': 0.12.2
'@oxlint-tsgolint/linux-x64': 0.12.2
'@oxlint-tsgolint/win32-arm64': 0.12.2
'@oxlint-tsgolint/win32-x64': 0.12.2
'@oxlint-tsgolint/darwin-arm64': 0.13.0
'@oxlint-tsgolint/darwin-x64': 0.13.0
'@oxlint-tsgolint/linux-arm64': 0.13.0
'@oxlint-tsgolint/linux-x64': 0.13.0
'@oxlint-tsgolint/win32-arm64': 0.13.0
'@oxlint-tsgolint/win32-x64': 0.13.0
oxlint@1.47.0(oxlint-tsgolint@0.12.2):
oxlint@1.47.0(oxlint-tsgolint@0.13.0):
optionalDependencies:
'@oxlint/binding-android-arm-eabi': 1.47.0
'@oxlint/binding-android-arm64': 1.47.0
@@ -10415,7 +10415,7 @@ snapshots:
'@oxlint/binding-win32-arm64-msvc': 1.47.0
'@oxlint/binding-win32-ia32-msvc': 1.47.0
'@oxlint/binding-win32-x64-msvc': 1.47.0
oxlint-tsgolint: 0.12.2
oxlint-tsgolint: 0.13.0
p-finally@1.0.0: {}
@@ -10791,7 +10791,7 @@ snapshots:
dependencies:
glob: 10.5.0
rolldown-plugin-dts@0.22.1(@typescript/native-preview@7.0.0-dev.20260214.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3):
rolldown-plugin-dts@0.22.1(@typescript/native-preview@7.0.0-dev.20260215.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3):
dependencies:
'@babel/generator': 8.0.0-rc.1
'@babel/helper-validator-identifier': 8.0.0-rc.1
@@ -10804,7 +10804,7 @@ snapshots:
obug: 2.1.1
rolldown: 1.0.0-rc.3
optionalDependencies:
'@typescript/native-preview': 7.0.0-dev.20260214.1
'@typescript/native-preview': 7.0.0-dev.20260215.1
typescript: 5.9.3
transitivePeerDependencies:
- oxc-resolver
@@ -11211,7 +11211,7 @@ snapshots:
array-back: 6.2.2
wordwrapjs: 5.1.1
tar@7.5.7:
tar@7.5.9:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
@@ -11269,7 +11269,7 @@ snapshots:
ts-algebra@2.0.0: {}
tsdown@0.20.3(@typescript/native-preview@7.0.0-dev.20260214.1)(typescript@5.9.3):
tsdown@0.20.3(@typescript/native-preview@7.0.0-dev.20260215.1)(typescript@5.9.3):
dependencies:
ansis: 4.2.0
cac: 6.7.14
@@ -11280,7 +11280,7 @@ snapshots:
obug: 2.1.1
picomatch: 4.0.3
rolldown: 1.0.0-rc.3
rolldown-plugin-dts: 0.22.1(@typescript/native-preview@7.0.0-dev.20260214.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3)
rolldown-plugin-dts: 0.22.1(@typescript/native-preview@7.0.0-dev.20260215.1)(rolldown@1.0.0-rc.3)(typescript@5.9.3)
semver: 7.7.4
tinyexec: 1.0.2
tinyglobby: 0.2.15

View File

@@ -15,6 +15,8 @@ COPY skills ./skills
COPY patches ./patches
COPY ui ./ui
COPY extensions/memory-core ./extensions/memory-core
COPY vendor/a2ui/renderers/lit ./vendor/a2ui/renderers/lit
COPY apps/shared/OpenClawKit/Tools/CanvasA2UI ./apps/shared/OpenClawKit/Tools/CanvasA2UI
RUN pnpm install --frozen-lockfile
RUN pnpm build

View File

@@ -0,0 +1,39 @@
#!/usr/bin/env node
import path from "node:path";
/**
* Prints selected files as NUL-delimited tokens to stdout.
*
* Usage:
* node scripts/pre-commit/filter-staged-files.mjs lint -- <files...>
* node scripts/pre-commit/filter-staged-files.mjs format -- <files...>
*
* Keep this dependency-free: the pre-commit hook runs in many environments.
*/
const mode = process.argv[2];
const rawArgs = process.argv.slice(3);
const files = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
if (mode !== "lint" && mode !== "format") {
process.stderr.write("usage: filter-staged-files.mjs <lint|format> -- <files...>\n");
process.exit(2);
}
const lintExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
const formatExts = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".md", ".mdx"]);
const shouldSelect = (filePath) => {
const ext = path.extname(filePath).toLowerCase();
if (mode === "lint") {
return lintExts.has(ext);
}
return formatExts.has(ext);
};
for (const file of files) {
if (shouldSelect(file)) {
process.stdout.write(file);
process.stdout.write("\0");
}
}

View File

@@ -27,8 +27,16 @@ const unitIsolatedFilesRaw = [
"src/browser/server.agent-contract-form-layout-act-commands.test.ts",
"src/browser/server.skips-default-maxchars-explicitly-set-zero.test.ts",
"src/browser/server.auth-token-gates-http.test.ts",
"src/browser/server-context.remote-tab-ops.test.ts",
"src/browser/server-context.ensure-tab-available.prefers-last-target.test.ts",
// Keep this high-variance heavy file off the unit-fast critical path.
"src/auto-reply/reply.block-streaming.test.ts",
// Archive extraction/fixture-heavy suite; keep off unit-fast critical path.
"src/hooks/install.test.ts",
// Setup-heavy bot bootstrap suite.
"src/telegram/bot.create-telegram-bot.test.ts",
// Medium-heavy bot behavior suite; move off unit-fast critical path.
"src/telegram/bot.test.ts",
// Slack slash registration tests are setup-heavy and can bottleneck unit-fast.
"src/slack/monitor/slash.test.ts",
// Uses process-level unhandledRejection listeners; keep it off vmForks to avoid cross-file leakage.
"src/imessage/monitor.shutdown.unhandled-rejection.test.ts",
];
@@ -165,7 +173,7 @@ const defaultWorkerBudget =
unit: Math.max(2, Math.min(8, Math.floor(localWorkers / 2))),
unitIsolated: 1,
extensions: Math.max(1, Math.min(4, Math.floor(localWorkers / 4))),
gateway: 1,
gateway: 2,
};
// Keep worker counts predictable for local runs; trim macOS CI workers to avoid worker crashes/OOM.

View File

@@ -18,7 +18,7 @@ metadata:
"archive": "tar.bz2",
"extract": true,
"stripComponents": 1,
"targetDir": "~/.openclaw/tools/sherpa-onnx-tts/runtime",
"targetDir": "runtime",
"label": "Download sherpa-onnx runtime (macOS)",
},
{
@@ -29,7 +29,7 @@ metadata:
"archive": "tar.bz2",
"extract": true,
"stripComponents": 1,
"targetDir": "~/.openclaw/tools/sherpa-onnx-tts/runtime",
"targetDir": "runtime",
"label": "Download sherpa-onnx runtime (Linux x64)",
},
{
@@ -40,7 +40,7 @@ metadata:
"archive": "tar.bz2",
"extract": true,
"stripComponents": 1,
"targetDir": "~/.openclaw/tools/sherpa-onnx-tts/runtime",
"targetDir": "runtime",
"label": "Download sherpa-onnx runtime (Windows x64)",
},
{
@@ -49,7 +49,7 @@ metadata:
"url": "https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/vits-piper-en_US-lessac-high.tar.bz2",
"archive": "tar.bz2",
"extract": true,
"targetDir": "~/.openclaw/tools/sherpa-onnx-tts/models",
"targetDir": "models",
"label": "Download Piper en_US lessac (high)",
},
],

View File

@@ -1,6 +1,7 @@
import type { RequestPermissionRequest } from "@agentclientprotocol/sdk";
import { describe, expect, it, vi } from "vitest";
import { resolvePermissionRequest } from "./client.js";
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
function makePermissionRequest(
overrides: Partial<RequestPermissionRequest> = {},
@@ -139,3 +140,32 @@ describe("resolvePermissionRequest", () => {
expect(res).toEqual({ outcome: { outcome: "cancelled" } });
});
});
describe("acp event mapper", () => {
it("extracts text and resource blocks into prompt text", () => {
const text = extractTextFromPrompt([
{ type: "text", text: "Hello" },
{ type: "resource", resource: { text: "File contents" } },
{ type: "resource_link", uri: "https://example.com", title: "Spec" },
{ type: "image", data: "abc", mimeType: "image/png" },
]);
expect(text).toBe("Hello\nFile contents\n[Resource link (Spec)] https://example.com");
});
it("extracts image blocks into gateway attachments", () => {
const attachments = extractAttachmentsFromPrompt([
{ type: "image", data: "abc", mimeType: "image/png" },
{ type: "image", data: "", mimeType: "image/png" },
{ type: "text", text: "ignored" },
]);
expect(attachments).toEqual([
{
type: "image",
mimeType: "image/png",
content: "abc",
},
]);
});
});

View File

@@ -1,31 +0,0 @@
import { describe, expect, it } from "vitest";
import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js";
describe("acp event mapper", () => {
it("extracts text and resource blocks into prompt text", () => {
const text = extractTextFromPrompt([
{ type: "text", text: "Hello" },
{ type: "resource", resource: { text: "File contents" } },
{ type: "resource_link", uri: "https://example.com", title: "Spec" },
{ type: "image", data: "abc", mimeType: "image/png" },
]);
expect(text).toBe("Hello\nFile contents\n[Resource link (Spec)] https://example.com");
});
it("extracts image blocks into gateway attachments", () => {
const attachments = extractAttachmentsFromPrompt([
{ type: "image", data: "abc", mimeType: "image/png" },
{ type: "image", data: "", mimeType: "image/png" },
{ type: "text", text: "ignored" },
]);
expect(attachments).toEqual([
{
type: "image",
mimeType: "image/png",
content: "abc",
},
]);
});
});

View File

@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { GatewayClient } from "../gateway/client.js";
import { parseSessionMeta, resolveSessionKey } from "./session-mapper.js";
import { createInMemorySessionStore } from "./session.js";
function createGateway(resolveLabelKey = "agent:main:label"): {
gateway: GatewayClient;
@@ -54,3 +55,26 @@ describe("acp session mapper", () => {
expect(request).not.toHaveBeenCalled();
});
});
describe("acp session manager", () => {
const store = createInMemorySessionStore();
afterEach(() => {
store.clearAllSessionsForTest();
});
it("tracks active runs and clears on cancel", () => {
const session = store.createSession({
sessionKey: "acp:test",
cwd: "/tmp",
});
const controller = new AbortController();
store.setActiveRun(session.sessionId, "run-1", controller);
expect(store.getSessionByRunId("run-1")?.sessionId).toBe(session.sessionId);
const cancelled = store.cancelActiveRun(session.sessionId);
expect(cancelled).toBe(true);
expect(store.getSessionByRunId("run-1")).toBeUndefined();
});
});

View File

@@ -1,25 +0,0 @@
import { describe, expect, it, afterEach } from "vitest";
import { createInMemorySessionStore } from "./session.js";
describe("acp session manager", () => {
const store = createInMemorySessionStore();
afterEach(() => {
store.clearAllSessionsForTest();
});
it("tracks active runs and clears on cancel", () => {
const session = store.createSession({
sessionKey: "acp:test",
cwd: "/tmp",
});
const controller = new AbortController();
store.setActiveRun(session.sessionId, "run-1", controller);
expect(store.getSessionByRunId("run-1")?.sessionId).toBe(session.sessionId);
const cancelled = store.cancelActiveRun(session.sessionId);
expect(cancelled).toBe(true);
expect(store.getSessionByRunId("run-1")).toBeUndefined();
});
});

View File

@@ -7,6 +7,7 @@ import {
parseAgentSessionKey,
} from "../routing/session-key.js";
import { resolveUserPath } from "../utils.js";
import { normalizeSkillFilter } from "./skills/filter.js";
import { resolveDefaultAgentWorkspaceDir } from "./workspace.js";
export { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
@@ -128,12 +129,7 @@ export function resolveAgentSkillsFilter(
cfg: OpenClawConfig,
agentId: string,
): string[] | undefined {
const raw = resolveAgentConfig(cfg, agentId)?.skills;
if (!raw) {
return undefined;
}
const normalized = raw.map((entry) => String(entry).trim()).filter(Boolean);
return normalized.length > 0 ? normalized : [];
return normalizeSkillFilter(resolveAgentConfig(cfg, agentId)?.skills);
}
export function resolveAgentModelPrimary(cfg: OpenClawConfig, agentId: string): string | undefined {

View File

@@ -1,12 +0,0 @@
import { describe, expect, it } from "vitest";
import { calculateAuthProfileCooldownMs } from "./auth-profiles.js";
describe("auth profile cooldowns", () => {
it("applies exponential backoff with a 1h cap", () => {
expect(calculateAuthProfileCooldownMs(1)).toBe(60_000);
expect(calculateAuthProfileCooldownMs(2)).toBe(5 * 60_000);
expect(calculateAuthProfileCooldownMs(3)).toBe(25 * 60_000);
expect(calculateAuthProfileCooldownMs(4)).toBe(60 * 60_000);
expect(calculateAuthProfileCooldownMs(5)).toBe(60 * 60_000);
});
});

View File

@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { captureEnv } from "../test-utils/env.js";
import {
type AuthProfileStore,
ensureAuthProfileStore,
@@ -10,10 +11,7 @@ import {
import { CHUTES_TOKEN_ENDPOINT, type ChutesStoredOAuth } from "./chutes-oauth.js";
describe("auth-profiles (chutes)", () => {
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
const previousChutesClientId = process.env.CHUTES_CLIENT_ID;
let envSnapshot: ReturnType<typeof captureEnv> | undefined;
let tempDir: string | null = null;
afterEach(async () => {
@@ -22,29 +20,17 @@ describe("auth-profiles (chutes)", () => {
await fs.rm(tempDir, { recursive: true, force: true });
tempDir = null;
}
if (previousStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousStateDir;
}
if (previousAgentDir === undefined) {
delete process.env.OPENCLAW_AGENT_DIR;
} else {
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
}
if (previousPiAgentDir === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
}
if (previousChutesClientId === undefined) {
delete process.env.CHUTES_CLIENT_ID;
} else {
process.env.CHUTES_CLIENT_ID = previousChutesClientId;
}
envSnapshot?.restore();
envSnapshot = undefined;
});
it("refreshes expired Chutes OAuth credentials", async () => {
envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
"CHUTES_CLIENT_ID",
]);
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-chutes-"));
process.env.OPENCLAW_STATE_DIR = tempDir;
process.env.OPENCLAW_AGENT_DIR = path.join(tempDir, "agents", "main", "agent");

View File

@@ -0,0 +1,77 @@
import { describe, expect, it } from "vitest";
import type { AuthProfileStore } from "./auth-profiles.js";
import { getSoonestCooldownExpiry } from "./auth-profiles.js";
function makeStore(usageStats?: AuthProfileStore["usageStats"]): AuthProfileStore {
return {
version: 1,
profiles: {},
usageStats,
};
}
describe("getSoonestCooldownExpiry", () => {
it("returns null when no cooldown timestamps exist", () => {
const store = makeStore();
expect(getSoonestCooldownExpiry(store, ["openai:p1"])).toBeNull();
});
it("returns earliest unusable time across profiles", () => {
const store = makeStore({
"openai:p1": {
cooldownUntil: 1_700_000_002_000,
disabledUntil: 1_700_000_004_000,
},
"openai:p2": {
cooldownUntil: 1_700_000_003_000,
},
"openai:p3": {
disabledUntil: 1_700_000_001_000,
},
});
expect(getSoonestCooldownExpiry(store, ["openai:p1", "openai:p2", "openai:p3"])).toBe(
1_700_000_001_000,
);
});
it("ignores unknown profiles and invalid cooldown values", () => {
const store = makeStore({
"openai:p1": {
cooldownUntil: -1,
},
"openai:p2": {
cooldownUntil: Infinity,
},
"openai:p3": {
disabledUntil: NaN,
},
"openai:p4": {
cooldownUntil: 1_700_000_005_000,
},
});
expect(
getSoonestCooldownExpiry(store, [
"missing",
"openai:p1",
"openai:p2",
"openai:p3",
"openai:p4",
]),
).toBe(1_700_000_005_000);
});
it("returns past timestamps when cooldown already expired", () => {
const store = makeStore({
"openai:p1": {
cooldownUntil: 1_700_000_000_000,
},
"openai:p2": {
disabledUntil: 1_700_000_010_000,
},
});
expect(getSoonestCooldownExpiry(store, ["openai:p1", "openai:p2"])).toBe(1_700_000_000_000);
});
});

View File

@@ -2,7 +2,11 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { ensureAuthProfileStore, markAuthProfileFailure } from "./auth-profiles.js";
import {
calculateAuthProfileCooldownMs,
ensureAuthProfileStore,
markAuthProfileFailure,
} from "./auth-profiles.js";
describe("markAuthProfileFailure", () => {
it("disables billing failures for ~5 hours by default", async () => {
@@ -129,3 +133,13 @@ describe("markAuthProfileFailure", () => {
}
});
});
describe("calculateAuthProfileCooldownMs", () => {
it("applies exponential backoff with a 1h cap", () => {
expect(calculateAuthProfileCooldownMs(1)).toBe(60_000);
expect(calculateAuthProfileCooldownMs(2)).toBe(5 * 60_000);
expect(calculateAuthProfileCooldownMs(3)).toBe(25 * 60_000);
expect(calculateAuthProfileCooldownMs(4)).toBe(60 * 60_000);
expect(calculateAuthProfileCooldownMs(5)).toBe(60 * 60_000);
});
});

View File

@@ -33,6 +33,7 @@ export type {
export {
calculateAuthProfileCooldownMs,
clearAuthProfileCooldown,
getSoonestCooldownExpiry,
isProfileInCooldown,
markAuthProfileCooldown,
markAuthProfileFailure,

View File

@@ -3,13 +3,16 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "./types.js";
import { captureEnv } from "../../test-utils/env.js";
import { resolveApiKeyForProfile } from "./oauth.js";
import { ensureAuthProfileStore } from "./store.js";
describe("resolveApiKeyForProfile fallback to main agent", () => {
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
const previousAgentDir = process.env.OPENCLAW_AGENT_DIR;
const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
const envSnapshot = captureEnv([
"OPENCLAW_STATE_DIR",
"OPENCLAW_AGENT_DIR",
"PI_CODING_AGENT_DIR",
]);
let tmpDir: string;
let mainAgentDir: string;
let secondaryAgentDir: string;
@@ -30,22 +33,7 @@ describe("resolveApiKeyForProfile fallback to main agent", () => {
afterEach(async () => {
vi.unstubAllGlobals();
// Restore original environment
if (previousStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousStateDir;
}
if (previousAgentDir === undefined) {
delete process.env.OPENCLAW_AGENT_DIR;
} else {
process.env.OPENCLAW_AGENT_DIR = previousAgentDir;
}
if (previousPiAgentDir === undefined) {
delete process.env.PI_CODING_AGENT_DIR;
} else {
process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
}
envSnapshot.restore();
await fs.rm(tmpDir, { recursive: true, force: true });
});

View File

@@ -25,6 +25,32 @@ export function isProfileInCooldown(store: AuthProfileStore, profileId: string):
return unusableUntil ? Date.now() < unusableUntil : false;
}
/**
* Return the soonest `unusableUntil` timestamp (ms epoch) among the given
* profiles, or `null` when no profile has a recorded cooldown. Note: the
* returned timestamp may be in the past if the cooldown has already expired.
*/
export function getSoonestCooldownExpiry(
store: AuthProfileStore,
profileIds: string[],
): number | null {
let soonest: number | null = null;
for (const id of profileIds) {
const stats = store.usageStats?.[id];
if (!stats) {
continue;
}
const until = resolveProfileUnusableUntil(stats);
if (typeof until !== "number" || !Number.isFinite(until) || until <= 0) {
continue;
}
if (soonest === null || until < soonest) {
soonest = until;
}
}
return soonest;
}
/**
* Mark a profile as successfully used. Resets error count and updates lastUsed.
* Uses store lock to avoid overwriting concurrent usage updates.

View File

@@ -1,17 +1,17 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { Type } from "@sinclair/typebox";
import path from "node:path";
import type { ExecAsk, ExecHost, ExecSecurity } from "../infra/exec-approvals.js";
import type { ProcessSession, SessionStdin } from "./bash-process-registry.js";
import type { ProcessSession } from "./bash-process-registry.js";
import type { ExecToolDetails } from "./bash-tools.exec.js";
import type { BashSandboxConfig } from "./bash-tools.shared.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { mergePathPrepend } from "../infra/path-prepend.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
export { applyPathPrepend, normalizePathPrepend } from "../infra/path-prepend.js";
import type { ManagedRun } from "../process/supervisor/index.js";
import { logWarn } from "../logger.js";
import { formatSpawnError, spawnWithFallback } from "../process/spawn-utils.js";
import { getProcessSupervisor } from "../process/supervisor/index.js";
import {
addSession,
appendOutput,
@@ -23,7 +23,6 @@ import {
buildDockerExecArgs,
chunkString,
clampWithDefault,
killSession,
readEnvInt,
} from "./bash-tools.shared.js";
import { buildCursorPositionResponse, stripDsrRequests } from "./pty-dsr.js";
@@ -147,26 +146,6 @@ export const execSchema = Type.Object({
),
});
type PtyExitEvent = { exitCode: number; signal?: number };
type PtyListener<T> = (event: T) => void;
type PtyHandle = {
pid: number;
write: (data: string | Buffer) => void;
onData: (listener: PtyListener<string>) => void;
onExit: (listener: PtyListener<PtyExitEvent>) => void;
};
type PtySpawn = (
file: string,
args: string[] | string,
options: {
name?: string;
cols?: number;
rows?: number;
cwd?: string;
env?: Record<string, string>;
},
) => PtyHandle;
export type ExecProcessOutcome = {
status: "completed" | "failed";
exitCode: number | null;
@@ -319,138 +298,10 @@ export async function runExecProcess(opts: {
}): Promise<ExecProcessHandle> {
const startedAt = Date.now();
const sessionId = createSessionSlug();
let child: ChildProcessWithoutNullStreams | null = null;
let pty: PtyHandle | null = null;
let stdin: SessionStdin | undefined;
const execCommand = opts.execCommand ?? opts.command;
const supervisor = getProcessSupervisor();
const spawnFallbacks = [
{
label: "no-detach",
options: { detached: false },
},
];
const handleSpawnFallback = (err: unknown, fallback: { label: string }) => {
const errText = formatSpawnError(err);
const warning = `Warning: spawn failed (${errText}); retrying with ${fallback.label}.`;
logWarn(`exec: spawn failed (${errText}); retrying with ${fallback.label}.`);
opts.warnings.push(warning);
};
const spawnShellChild = async (
shell: string,
shellArgs: string[],
): Promise<ChildProcessWithoutNullStreams> => {
const { child: spawned } = await spawnWithFallback({
argv: [shell, ...shellArgs, execCommand],
options: {
cwd: opts.workdir,
env: opts.env,
detached: process.platform !== "win32",
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
},
fallbacks: spawnFallbacks,
onFallback: handleSpawnFallback,
});
return spawned as ChildProcessWithoutNullStreams;
};
// `exec` does not currently accept tool-provided stdin content. For non-PTY runs,
// keeping stdin open can cause commands like `wc -l` (or safeBins-hardened segments)
// to block forever waiting for input, leading to accidental backgrounding.
// For interactive flows, callers should use `pty: true` (stdin kept open).
const maybeCloseNonPtyStdin = () => {
if (opts.usePty) {
return;
}
try {
// Signal EOF immediately so stdin-only commands can terminate.
child?.stdin?.end();
} catch {
// ignore stdin close errors
}
};
if (opts.sandbox) {
const { child: spawned } = await spawnWithFallback({
argv: [
"docker",
...buildDockerExecArgs({
containerName: opts.sandbox.containerName,
command: execCommand,
workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
env: opts.env,
tty: opts.usePty,
}),
],
options: {
cwd: opts.workdir,
env: process.env,
detached: process.platform !== "win32",
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
},
fallbacks: spawnFallbacks,
onFallback: handleSpawnFallback,
});
child = spawned as ChildProcessWithoutNullStreams;
stdin = child.stdin;
maybeCloseNonPtyStdin();
} else if (opts.usePty) {
const { shell, args: shellArgs } = getShellConfig();
try {
const ptyModule = (await import("@lydell/node-pty")) as unknown as {
spawn?: PtySpawn;
default?: { spawn?: PtySpawn };
};
const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
if (!spawnPty) {
throw new Error("PTY support is unavailable (node-pty spawn not found).");
}
pty = spawnPty(shell, [...shellArgs, execCommand], {
cwd: opts.workdir,
env: opts.env,
name: process.env.TERM ?? "xterm-256color",
cols: 120,
rows: 30,
});
stdin = {
destroyed: false,
write: (data, cb) => {
try {
pty?.write(data);
cb?.(null);
} catch (err) {
cb?.(err as Error);
}
},
end: () => {
try {
const eof = process.platform === "win32" ? "\x1a" : "\x04";
pty?.write(eof);
} catch {
// ignore EOF errors
}
},
};
} catch (err) {
const errText = String(err);
const warning = `Warning: PTY spawn failed (${errText}); retrying without PTY for \`${opts.command}\`.`;
logWarn(`exec: PTY spawn failed (${errText}); retrying without PTY for "${opts.command}".`);
opts.warnings.push(warning);
child = await spawnShellChild(shell, shellArgs);
stdin = child.stdin;
}
} else {
const { shell, args: shellArgs } = getShellConfig();
child = await spawnShellChild(shell, shellArgs);
stdin = child.stdin;
maybeCloseNonPtyStdin();
}
const session = {
const session: ProcessSession = {
id: sessionId,
command: opts.command,
scopeKey: opts.scopeKey,
@@ -458,9 +309,9 @@ export async function runExecProcess(opts: {
notifyOnExit: opts.notifyOnExit,
notifyOnExitEmptySuccess: opts.notifyOnExitEmptySuccess === true,
exitNotified: false,
child: child ?? undefined,
stdin,
pid: child?.pid ?? pty?.pid,
child: undefined,
stdin: undefined,
pid: undefined,
startedAt,
cwd: opts.workdir,
maxOutputChars: opts.maxOutput,
@@ -477,59 +328,9 @@ export async function runExecProcess(opts: {
exitSignal: undefined as NodeJS.Signals | number | null | undefined,
truncated: false,
backgrounded: false,
} satisfies ProcessSession;
};
addSession(session);
let settled = false;
let timeoutTimer: NodeJS.Timeout | null = null;
let timeoutFinalizeTimer: NodeJS.Timeout | null = null;
let timedOut = false;
const timeoutFinalizeMs = 1000;
let resolveFn: ((outcome: ExecProcessOutcome) => void) | null = null;
const settle = (outcome: ExecProcessOutcome) => {
if (settled) {
return;
}
settled = true;
resolveFn?.(outcome);
};
const finalizeTimeout = () => {
if (session.exited) {
return;
}
markExited(session, null, "SIGKILL", "failed");
maybeNotifyOnExit(session, "failed");
const aggregated = session.aggregated.trim();
const reason = `Command timed out after ${opts.timeoutSec} seconds`;
settle({
status: "failed",
exitCode: null,
exitSignal: "SIGKILL",
durationMs: Date.now() - startedAt,
aggregated,
timedOut: true,
reason: aggregated ? `${aggregated}\n\n${reason}` : reason,
});
};
const onTimeout = () => {
timedOut = true;
killSession(session);
if (!timeoutFinalizeTimer) {
timeoutFinalizeTimer = setTimeout(() => {
finalizeTimeout();
}, timeoutFinalizeMs);
}
};
if (opts.timeoutSec > 0) {
timeoutTimer = setTimeout(() => {
onTimeout();
}, opts.timeoutSec * 1000);
}
const emitUpdate = () => {
if (!opts.onUpdate) {
return;
@@ -565,116 +366,208 @@ export async function runExecProcess(opts: {
}
};
if (pty) {
const cursorResponse = buildCursorPositionResponse();
pty.onData((data) => {
const raw = data.toString();
const { cleaned, requests } = stripDsrRequests(raw);
if (requests > 0) {
const timeoutMs =
typeof opts.timeoutSec === "number" && opts.timeoutSec > 0
? Math.floor(opts.timeoutSec * 1000)
: undefined;
const spawnSpec:
| {
mode: "child";
argv: string[];
env: NodeJS.ProcessEnv;
stdinMode: "pipe-open" | "pipe-closed";
}
| {
mode: "pty";
ptyCommand: string;
childFallbackArgv: string[];
env: NodeJS.ProcessEnv;
stdinMode: "pipe-open";
} = (() => {
if (opts.sandbox) {
return {
mode: "child" as const,
argv: [
"docker",
...buildDockerExecArgs({
containerName: opts.sandbox.containerName,
command: execCommand,
workdir: opts.containerWorkdir ?? opts.sandbox.containerWorkdir,
env: opts.env,
tty: opts.usePty,
}),
],
env: process.env,
stdinMode: opts.usePty ? ("pipe-open" as const) : ("pipe-closed" as const),
};
}
const { shell, args: shellArgs } = getShellConfig();
const childArgv = [shell, ...shellArgs, execCommand];
if (opts.usePty) {
return {
mode: "pty" as const,
ptyCommand: execCommand,
childFallbackArgv: childArgv,
env: opts.env,
stdinMode: "pipe-open" as const,
};
}
return {
mode: "child" as const,
argv: childArgv,
env: opts.env,
stdinMode: "pipe-closed" as const,
};
})();
let managedRun: ManagedRun | null = null;
let usingPty = spawnSpec.mode === "pty";
const cursorResponse = buildCursorPositionResponse();
const onSupervisorStdout = (chunk: string) => {
if (usingPty) {
const { cleaned, requests } = stripDsrRequests(chunk);
if (requests > 0 && managedRun?.stdin) {
for (let i = 0; i < requests; i += 1) {
pty.write(cursorResponse);
managedRun.stdin.write(cursorResponse);
}
}
handleStdout(cleaned);
});
} else if (child) {
child.stdout.on("data", handleStdout);
child.stderr.on("data", handleStderr);
}
return;
}
handleStdout(chunk);
};
const promise = new Promise<ExecProcessOutcome>((resolve) => {
resolveFn = resolve;
const handleExit = (code: number | null, exitSignal: NodeJS.Signals | number | null) => {
if (timeoutTimer) {
clearTimeout(timeoutTimer);
}
if (timeoutFinalizeTimer) {
clearTimeout(timeoutFinalizeTimer);
try {
const spawnBase = {
runId: sessionId,
sessionId: opts.sessionKey?.trim() || sessionId,
backendId: opts.sandbox ? "exec-sandbox" : "exec-host",
scopeKey: opts.scopeKey,
cwd: opts.workdir,
env: spawnSpec.env,
timeoutMs,
captureOutput: false,
onStdout: onSupervisorStdout,
onStderr: handleStderr,
};
managedRun =
spawnSpec.mode === "pty"
? await supervisor.spawn({
...spawnBase,
mode: "pty",
ptyCommand: spawnSpec.ptyCommand,
})
: await supervisor.spawn({
...spawnBase,
mode: "child",
argv: spawnSpec.argv,
stdinMode: spawnSpec.stdinMode,
});
} catch (err) {
if (spawnSpec.mode === "pty") {
const warning = `Warning: PTY spawn failed (${String(err)}); retrying without PTY for \`${opts.command}\`.`;
logWarn(
`exec: PTY spawn failed (${String(err)}); retrying without PTY for "${opts.command}".`,
);
opts.warnings.push(warning);
usingPty = false;
try {
managedRun = await supervisor.spawn({
runId: sessionId,
sessionId: opts.sessionKey?.trim() || sessionId,
backendId: "exec-host",
scopeKey: opts.scopeKey,
mode: "child",
argv: spawnSpec.childFallbackArgv,
cwd: opts.workdir,
env: spawnSpec.env,
stdinMode: "pipe-open",
timeoutMs,
captureOutput: false,
onStdout: handleStdout,
onStderr: handleStderr,
});
} catch (retryErr) {
markExited(session, null, null, "failed");
maybeNotifyOnExit(session, "failed");
throw retryErr;
}
} else {
markExited(session, null, null, "failed");
maybeNotifyOnExit(session, "failed");
throw err;
}
}
session.stdin = managedRun.stdin;
session.pid = managedRun.pid;
const promise = managedRun
.wait()
.then((exit): ExecProcessOutcome => {
const durationMs = Date.now() - startedAt;
const wasSignal = exitSignal != null;
const isSuccess = code === 0 && !wasSignal && !timedOut;
const status: "completed" | "failed" = isSuccess ? "completed" : "failed";
markExited(session, code, exitSignal, status);
const status: "completed" | "failed" =
exit.exitCode === 0 && exit.reason === "exit" ? "completed" : "failed";
markExited(session, exit.exitCode, exit.exitSignal, status);
maybeNotifyOnExit(session, status);
if (!session.child && session.stdin) {
session.stdin.destroyed = true;
}
if (settled) {
return;
}
const aggregated = session.aggregated.trim();
if (!isSuccess) {
const reason = timedOut
? `Command timed out after ${opts.timeoutSec} seconds`
: wasSignal && exitSignal
? `Command aborted by signal ${exitSignal}`
: code === null
? "Command aborted before exit code was captured"
: `Command exited with code ${code}`;
const message = aggregated ? `${aggregated}\n\n${reason}` : reason;
settle({
status: "failed",
exitCode: code ?? null,
exitSignal: exitSignal ?? null,
if (status === "completed") {
return {
status: "completed",
exitCode: exit.exitCode ?? 0,
exitSignal: exit.exitSignal,
durationMs,
aggregated,
timedOut,
reason: message,
});
return;
timedOut: false,
};
}
settle({
status: "completed",
exitCode: code ?? 0,
exitSignal: exitSignal ?? null,
const reason =
exit.reason === "overall-timeout"
? `Command timed out after ${opts.timeoutSec} seconds`
: exit.reason === "no-output-timeout"
? "Command timed out waiting for output"
: exit.exitSignal != null
? `Command aborted by signal ${exit.exitSignal}`
: exit.exitCode == null
? "Command aborted before exit code was captured"
: `Command exited with code ${exit.exitCode}`;
return {
status: "failed",
exitCode: exit.exitCode,
exitSignal: exit.exitSignal,
durationMs,
aggregated,
timedOut: exit.timedOut,
reason: aggregated ? `${aggregated}\n\n${reason}` : reason,
};
})
.catch((err): ExecProcessOutcome => {
markExited(session, null, null, "failed");
maybeNotifyOnExit(session, "failed");
const aggregated = session.aggregated.trim();
const message = aggregated ? `${aggregated}\n\n${String(err)}` : String(err);
return {
status: "failed",
exitCode: null,
exitSignal: null,
durationMs: Date.now() - startedAt,
aggregated,
timedOut: false,
});
};
if (pty) {
pty.onExit((event) => {
const rawSignal = event.signal ?? null;
const normalizedSignal = rawSignal === 0 ? null : rawSignal;
handleExit(event.exitCode ?? null, normalizedSignal);
});
} else if (child) {
child.once("close", (code, exitSignal) => {
handleExit(code, exitSignal);
});
child.once("error", (err) => {
if (timeoutTimer) {
clearTimeout(timeoutTimer);
}
if (timeoutFinalizeTimer) {
clearTimeout(timeoutFinalizeTimer);
}
markExited(session, null, null, "failed");
maybeNotifyOnExit(session, "failed");
const aggregated = session.aggregated.trim();
const message = aggregated ? `${aggregated}\n\n${String(err)}` : String(err);
settle({
status: "failed",
exitCode: null,
exitSignal: null,
durationMs: Date.now() - startedAt,
aggregated,
timedOut,
reason: message,
});
});
}
});
reason: message,
};
});
return {
session,
startedAt,
pid: session.pid ?? undefined,
promise,
kill: () => killSession(session),
kill: () => {
managedRun?.cancel("manual-cancel");
},
};
}

View File

@@ -0,0 +1,71 @@
import { afterEach, expect, test, vi } from "vitest";
import { resetProcessRegistryForTests } from "./bash-process-registry";
import { createExecTool } from "./bash-tools.exec";
const { ptySpawnMock } = vi.hoisted(() => ({
ptySpawnMock: vi.fn(),
}));
vi.mock("@lydell/node-pty", () => ({
spawn: (...args: unknown[]) => ptySpawnMock(...args),
}));
afterEach(() => {
resetProcessRegistryForTests();
vi.clearAllMocks();
});
test("exec disposes PTY listeners after normal exit", async () => {
const disposeData = vi.fn();
const disposeExit = vi.fn();
ptySpawnMock.mockImplementation(() => ({
pid: 0,
write: vi.fn(),
onData: (listener: (value: string) => void) => {
listener("ok");
return { dispose: disposeData };
},
onExit: (listener: (event: { exitCode: number; signal?: number }) => void) => {
listener({ exitCode: 0 });
return { dispose: disposeExit };
},
kill: vi.fn(),
}));
const tool = createExecTool({ allowBackground: false });
const result = await tool.execute("toolcall", {
command: "echo ok",
pty: true,
});
expect(result.details.status).toBe("completed");
expect(disposeData).toHaveBeenCalledTimes(1);
expect(disposeExit).toHaveBeenCalledTimes(1);
});
test("exec tears down PTY resources on timeout", async () => {
const disposeData = vi.fn();
const disposeExit = vi.fn();
const kill = vi.fn();
ptySpawnMock.mockImplementation(() => ({
pid: 0,
write: vi.fn(),
onData: () => ({ dispose: disposeData }),
onExit: () => ({ dispose: disposeExit }),
kill,
}));
const tool = createExecTool({ allowBackground: false });
await expect(
tool.execute("toolcall", {
command: "sleep 5",
pty: true,
timeout: 0.01,
}),
).rejects.toThrow("Command timed out");
expect(kill).toHaveBeenCalledTimes(1);
expect(disposeData).toHaveBeenCalledTimes(1);
expect(disposeExit).toHaveBeenCalledTimes(1);
});

View File

@@ -0,0 +1,39 @@
import { afterEach, expect, test, vi } from "vitest";
import { listRunningSessions, resetProcessRegistryForTests } from "./bash-process-registry";
import { createExecTool } from "./bash-tools.exec";
const { supervisorSpawnMock } = vi.hoisted(() => ({
supervisorSpawnMock: vi.fn(),
}));
vi.mock("../process/supervisor/index.js", () => ({
getProcessSupervisor: () => ({
spawn: (...args: unknown[]) => supervisorSpawnMock(...args),
cancel: vi.fn(),
cancelScope: vi.fn(),
reconcileOrphans: vi.fn(),
getRecord: vi.fn(),
}),
}));
afterEach(() => {
resetProcessRegistryForTests();
vi.clearAllMocks();
});
test("exec cleans session state when PTY fallback spawn also fails", async () => {
supervisorSpawnMock
.mockRejectedValueOnce(new Error("pty spawn failed"))
.mockRejectedValueOnce(new Error("child fallback failed"));
const tool = createExecTool({ allowBackground: false });
await expect(
tool.execute("toolcall", {
command: "echo ok",
pty: true,
}),
).rejects.toThrow("child fallback failed");
expect(listRunningSessions()).toHaveLength(0);
});

View File

@@ -0,0 +1,152 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ProcessSession } from "./bash-process-registry.js";
import {
addSession,
getFinishedSession,
getSession,
resetProcessRegistryForTests,
} from "./bash-process-registry.js";
import { createProcessTool } from "./bash-tools.process.js";
const { supervisorMock } = vi.hoisted(() => ({
supervisorMock: {
spawn: vi.fn(),
cancel: vi.fn(),
cancelScope: vi.fn(),
reconcileOrphans: vi.fn(),
getRecord: vi.fn(),
},
}));
const { killProcessTreeMock } = vi.hoisted(() => ({
killProcessTreeMock: vi.fn(),
}));
vi.mock("../process/supervisor/index.js", () => ({
getProcessSupervisor: () => supervisorMock,
}));
vi.mock("../process/kill-tree.js", () => ({
killProcessTree: (...args: unknown[]) => killProcessTreeMock(...args),
}));
function createBackgroundSession(id: string, pid?: number): ProcessSession {
return {
id,
command: "sleep 999",
startedAt: Date.now(),
cwd: "/tmp",
maxOutputChars: 10_000,
pendingMaxOutputChars: 30_000,
totalOutputChars: 0,
pendingStdout: [],
pendingStderr: [],
pendingStdoutChars: 0,
pendingStderrChars: 0,
aggregated: "",
tail: "",
pid,
exited: false,
exitCode: undefined,
exitSignal: undefined,
truncated: false,
backgrounded: true,
};
}
describe("process tool supervisor cancellation", () => {
beforeEach(() => {
supervisorMock.spawn.mockReset();
supervisorMock.cancel.mockReset();
supervisorMock.cancelScope.mockReset();
supervisorMock.reconcileOrphans.mockReset();
supervisorMock.getRecord.mockReset();
killProcessTreeMock.mockReset();
});
afterEach(() => {
resetProcessRegistryForTests();
});
it("routes kill through supervisor when run is managed", async () => {
supervisorMock.getRecord.mockReturnValue({
runId: "sess",
state: "running",
});
addSession(createBackgroundSession("sess"));
const processTool = createProcessTool();
const result = await processTool.execute("toolcall", {
action: "kill",
sessionId: "sess",
});
expect(supervisorMock.cancel).toHaveBeenCalledWith("sess", "manual-cancel");
expect(getSession("sess")).toBeDefined();
expect(getSession("sess")?.exited).toBe(false);
expect(result.content[0]).toMatchObject({
type: "text",
text: "Termination requested for session sess.",
});
});
it("remove drops running session immediately when cancellation is requested", async () => {
supervisorMock.getRecord.mockReturnValue({
runId: "sess",
state: "running",
});
addSession(createBackgroundSession("sess"));
const processTool = createProcessTool();
const result = await processTool.execute("toolcall", {
action: "remove",
sessionId: "sess",
});
expect(supervisorMock.cancel).toHaveBeenCalledWith("sess", "manual-cancel");
expect(getSession("sess")).toBeUndefined();
expect(getFinishedSession("sess")).toBeUndefined();
expect(result.content[0]).toMatchObject({
type: "text",
text: "Removed session sess (termination requested).",
});
});
it("falls back to process-tree kill when supervisor record is missing", async () => {
supervisorMock.getRecord.mockReturnValue(undefined);
addSession(createBackgroundSession("sess-fallback", 4242));
const processTool = createProcessTool();
const result = await processTool.execute("toolcall", {
action: "kill",
sessionId: "sess-fallback",
});
expect(killProcessTreeMock).toHaveBeenCalledWith(4242);
expect(getSession("sess-fallback")).toBeUndefined();
expect(getFinishedSession("sess-fallback")).toBeDefined();
expect(result.content[0]).toMatchObject({
type: "text",
text: "Killed session sess-fallback.",
});
});
it("fails remove when no supervisor record and no pid is available", async () => {
supervisorMock.getRecord.mockReturnValue(undefined);
addSession(createBackgroundSession("sess-no-pid"));
const processTool = createProcessTool();
const result = await processTool.execute("toolcall", {
action: "remove",
sessionId: "sess-no-pid",
});
expect(killProcessTreeMock).not.toHaveBeenCalled();
expect(getSession("sess-no-pid")).toBeDefined();
expect(result.details).toMatchObject({ status: "failed" });
expect(result.content[0]).toMatchObject({
type: "text",
text: "Unable to remove session sess-no-pid: no active supervisor run or process id.",
});
});
});

View File

@@ -1,7 +1,10 @@
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
import { Type } from "@sinclair/typebox";
import { formatDurationCompact } from "../infra/format-time/format-duration.ts";
import { killProcessTree } from "../process/kill-tree.js";
import { getProcessSupervisor } from "../process/supervisor/index.js";
import {
type ProcessSession,
deleteSession,
drainSession,
getFinishedSession,
@@ -11,13 +14,7 @@ import {
markExited,
setJobTtlMs,
} from "./bash-process-registry.js";
import {
deriveSessionName,
killSession,
pad,
sliceLogLines,
truncateMiddle,
} from "./bash-tools.shared.js";
import { deriveSessionName, pad, sliceLogLines, truncateMiddle } from "./bash-tools.shared.js";
import { encodeKeySequence, encodePaste } from "./pty-keys.js";
export type ProcessToolDefaults = {
@@ -65,8 +62,9 @@ const processSchema = Type.Object({
offset: Type.Optional(Type.Number({ description: "Log offset" })),
limit: Type.Optional(Type.Number({ description: "Log length" })),
timeout: Type.Optional(
Type.Union([Type.Number(), Type.String()], {
Type.Number({
description: "For poll: wait up to this many milliseconds before returning",
minimum: 0,
}),
),
});
@@ -106,9 +104,28 @@ export function createProcessTool(
setJobTtlMs(defaults.cleanupMs);
}
const scopeKey = defaults?.scopeKey;
const supervisor = getProcessSupervisor();
const isInScope = (session?: { scopeKey?: string } | null) =>
!scopeKey || session?.scopeKey === scopeKey;
const cancelManagedSession = (sessionId: string) => {
const record = supervisor.getRecord(sessionId);
if (!record || record.state === "exited") {
return false;
}
supervisor.cancel(sessionId, "manual-cancel");
return true;
};
const terminateSessionFallback = (session: ProcessSession) => {
const pid = session.pid ?? session.child?.pid;
if (typeof pid !== "number" || !Number.isFinite(pid) || pid <= 0) {
return false;
}
killProcessTree(pid);
return true;
};
return {
name: "process",
label: "process",
@@ -138,7 +155,7 @@ export function createProcessTool(
eof?: boolean;
offset?: number;
limit?: number;
timeout?: number | string;
timeout?: unknown;
};
if (params.action === "list") {
@@ -522,10 +539,25 @@ export function createProcessTool(
if (!scopedSession.backgrounded) {
return failText(`Session ${params.sessionId} is not backgrounded.`);
}
killSession(scopedSession);
markExited(scopedSession, null, "SIGKILL", "failed");
const canceled = cancelManagedSession(scopedSession.id);
if (!canceled) {
const terminated = terminateSessionFallback(scopedSession);
if (!terminated) {
return failText(
`Unable to terminate session ${params.sessionId}: no active supervisor run or process id.`,
);
}
markExited(scopedSession, null, "SIGKILL", "failed");
}
return {
content: [{ type: "text", text: `Killed session ${params.sessionId}.` }],
content: [
{
type: "text",
text: canceled
? `Termination requested for session ${params.sessionId}.`
: `Killed session ${params.sessionId}.`,
},
],
details: {
status: "failed",
name: scopedSession ? deriveSessionName(scopedSession.command) : undefined,
@@ -554,10 +586,30 @@ export function createProcessTool(
case "remove": {
if (scopedSession) {
killSession(scopedSession);
markExited(scopedSession, null, "SIGKILL", "failed");
const canceled = cancelManagedSession(scopedSession.id);
if (canceled) {
// Keep remove semantics deterministic: drop from process registry now.
scopedSession.backgrounded = false;
deleteSession(params.sessionId);
} else {
const terminated = terminateSessionFallback(scopedSession);
if (!terminated) {
return failText(
`Unable to remove session ${params.sessionId}: no active supervisor run or process id.`,
);
}
markExited(scopedSession, null, "SIGKILL", "failed");
deleteSession(params.sessionId);
}
return {
content: [{ type: "text", text: `Removed session ${params.sessionId}.` }],
content: [
{
type: "text",
text: canceled
? `Removed session ${params.sessionId} (termination requested).`
: `Removed session ${params.sessionId}.`,
},
],
details: {
status: "failed",
name: scopedSession ? deriveSessionName(scopedSession.command) : undefined,

View File

@@ -1,11 +1,9 @@
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { existsSync, statSync } from "node:fs";
import fs from "node:fs/promises";
import { homedir } from "node:os";
import path from "node:path";
import { sliceUtf16Safe } from "../utils.js";
import { assertSandboxPath } from "./sandbox-paths.js";
import { killProcessTree } from "./shell-utils.js";
const CHUNK_LIMIT = 8 * 1024;
@@ -115,13 +113,6 @@ export async function resolveSandboxWorkdir(params: {
}
}
export function killSession(session: { pid?: number; child?: ChildProcessWithoutNullStreams }) {
const pid = session.pid ?? session.child?.pid;
if (pid) {
killProcessTree(pid);
}
}
export function resolveWorkdir(workdir: string, warnings: string[]) {
const current = safeCwd();
const fallback = current ?? homedir();

View File

@@ -2,7 +2,19 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { sleep } from "../utils.js";
import { runClaudeCliAgent } from "./claude-cli-runner.js";
const runCommandWithTimeoutMock = vi.fn();
const mocks = vi.hoisted(() => ({
spawn: vi.fn(),
}));
vi.mock("../process/supervisor/index.js", () => ({
getProcessSupervisor: () => ({
spawn: (...args: unknown[]) => mocks.spawn(...args),
cancel: vi.fn(),
cancelScope: vi.fn(),
reconcileOrphans: async () => {},
getRecord: vi.fn(),
}),
}));
function createDeferred<T>() {
let resolve: (value: T) => void;
@@ -18,6 +30,40 @@ function createDeferred<T>() {
};
}
function createManagedRun(
exit: Promise<{
reason: "exit" | "overall-timeout" | "no-output-timeout" | "signal" | "manual-cancel";
exitCode: number | null;
exitSignal: NodeJS.Signals | null;
durationMs: number;
stdout: string;
stderr: string;
timedOut: boolean;
noOutputTimedOut: boolean;
}>,
) {
return {
runId: "run-test",
pid: 12345,
startedAtMs: Date.now(),
wait: async () => await exit,
cancel: vi.fn(),
};
}
function successExit(payload: { message: string; session_id: string }) {
return {
reason: "exit" as const,
exitCode: 0,
exitSignal: null,
durationMs: 1,
stdout: JSON.stringify(payload),
stderr: "",
timedOut: false,
noOutputTimedOut: false,
};
}
async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: number) {
for (let i = 0; i < 50; i += 1) {
if (mockFn.mock.calls.length >= count) {
@@ -28,23 +74,15 @@ async function waitForCalls(mockFn: { mock: { calls: unknown[][] } }, count: num
throw new Error(`Expected ${count} calls, got ${mockFn.mock.calls.length}`);
}
vi.mock("../process/exec.js", () => ({
runCommandWithTimeout: (...args: unknown[]) => runCommandWithTimeoutMock(...args),
}));
describe("runClaudeCliAgent", () => {
beforeEach(() => {
runCommandWithTimeoutMock.mockReset();
mocks.spawn.mockReset();
});
it("starts a new session with --session-id when none is provided", async () => {
runCommandWithTimeoutMock.mockResolvedValueOnce({
stdout: JSON.stringify({ message: "ok", session_id: "sid-1" }),
stderr: "",
code: 0,
signal: null,
killed: false,
});
mocks.spawn.mockResolvedValueOnce(
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-1" }))),
);
await runClaudeCliAgent({
sessionId: "openclaw-session",
@@ -56,21 +94,18 @@ describe("runClaudeCliAgent", () => {
runId: "run-1",
});
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
const argv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[];
expect(argv).toContain("claude");
expect(argv).toContain("--session-id");
expect(argv).toContain("hi");
expect(mocks.spawn).toHaveBeenCalledTimes(1);
const spawnInput = mocks.spawn.mock.calls[0]?.[0] as { argv: string[]; mode: string };
expect(spawnInput.mode).toBe("child");
expect(spawnInput.argv).toContain("claude");
expect(spawnInput.argv).toContain("--session-id");
expect(spawnInput.argv).toContain("hi");
});
it("uses --resume when a claude session id is provided", async () => {
runCommandWithTimeoutMock.mockResolvedValueOnce({
stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }),
stderr: "",
code: 0,
signal: null,
killed: false,
});
mocks.spawn.mockResolvedValueOnce(
createManagedRun(Promise.resolve(successExit({ message: "ok", session_id: "sid-2" }))),
);
await runClaudeCliAgent({
sessionId: "openclaw-session",
@@ -83,32 +118,21 @@ describe("runClaudeCliAgent", () => {
claudeSessionId: "c9d7b831-1c31-4d22-80b9-1e50ca207d4b",
});
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
const argv = runCommandWithTimeoutMock.mock.calls[0]?.[0] as string[];
expect(argv).toContain("--resume");
expect(argv).toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
expect(argv).toContain("hi");
expect(mocks.spawn).toHaveBeenCalledTimes(1);
const spawnInput = mocks.spawn.mock.calls[0]?.[0] as { argv: string[] };
expect(spawnInput.argv).toContain("--resume");
expect(spawnInput.argv).toContain("c9d7b831-1c31-4d22-80b9-1e50ca207d4b");
expect(spawnInput.argv).not.toContain("--session-id");
expect(spawnInput.argv).toContain("hi");
});
it("serializes concurrent claude-cli runs", async () => {
const firstDeferred = createDeferred<{
stdout: string;
stderr: string;
code: number | null;
signal: NodeJS.Signals | null;
killed: boolean;
}>();
const secondDeferred = createDeferred<{
stdout: string;
stderr: string;
code: number | null;
signal: NodeJS.Signals | null;
killed: boolean;
}>();
const firstDeferred = createDeferred<ReturnType<typeof successExit>>();
const secondDeferred = createDeferred<ReturnType<typeof successExit>>();
runCommandWithTimeoutMock
.mockImplementationOnce(() => firstDeferred.promise)
.mockImplementationOnce(() => secondDeferred.promise);
mocks.spawn
.mockResolvedValueOnce(createManagedRun(firstDeferred.promise))
.mockResolvedValueOnce(createManagedRun(secondDeferred.promise));
const firstRun = runClaudeCliAgent({
sessionId: "s1",
@@ -130,25 +154,13 @@ describe("runClaudeCliAgent", () => {
runId: "run-2",
});
await waitForCalls(runCommandWithTimeoutMock, 1);
await waitForCalls(mocks.spawn, 1);
firstDeferred.resolve({
stdout: JSON.stringify({ message: "ok", session_id: "sid-1" }),
stderr: "",
code: 0,
signal: null,
killed: false,
});
firstDeferred.resolve(successExit({ message: "ok", session_id: "sid-1" }));
await waitForCalls(runCommandWithTimeoutMock, 2);
await waitForCalls(mocks.spawn, 2);
secondDeferred.resolve({
stdout: JSON.stringify({ message: "ok", session_id: "sid-2" }),
stderr: "",
code: 0,
signal: null,
killed: false,
});
secondDeferred.resolve(successExit({ message: "ok", session_id: "sid-2" }));
await Promise.all([firstRun, secondRun]);
});

View File

@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolveCliBackendConfig } from "./cli-backends.js";
describe("resolveCliBackendConfig reliability merge", () => {
it("deep-merges reliability watchdog overrides for codex", () => {
const cfg = {
agents: {
defaults: {
cliBackends: {
"codex-cli": {
command: "codex",
reliability: {
watchdog: {
resume: {
noOutputTimeoutMs: 42_000,
},
},
},
},
},
},
},
} satisfies OpenClawConfig;
const resolved = resolveCliBackendConfig("codex-cli", cfg);
expect(resolved).not.toBeNull();
expect(resolved?.config.reliability?.watchdog?.resume?.noOutputTimeoutMs).toBe(42_000);
// Ensure defaults are retained when only one field is overridden.
expect(resolved?.config.reliability?.watchdog?.resume?.noOutputTimeoutRatio).toBe(0.3);
expect(resolved?.config.reliability?.watchdog?.resume?.minMs).toBe(60_000);
expect(resolved?.config.reliability?.watchdog?.resume?.maxMs).toBe(180_000);
expect(resolved?.config.reliability?.watchdog?.fresh?.noOutputTimeoutRatio).toBe(0.8);
});
});

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