Commit Graph

32119 Commits

Author SHA1 Message Date
Gustavo Madeira Santana
5d1fd2ea6a refactor(agents): clarify requester peer prefix handling 2026-04-18 14:01:39 -04:00
Gustavo Madeira Santana
91f0217cf6 fix(regression): propagate team scope into child spawns 2026-04-18 14:01:39 -04:00
Gustavo Madeira Santana
0b45ea937a refactor(routing): keep bound account scope generic 2026-04-18 14:01:39 -04:00
Gustavo Madeira Santana
c443de6507 fix(agents): route requester roles into child origins 2026-04-18 14:01:38 -04:00
Gustavo Madeira Santana
8a59771adb fix(agents): honor binding scope in child origins 2026-04-18 14:01:38 -04:00
Gustavo Madeira Santana
33fc14e3fe fix(agents): preserve plugin target kind inference 2026-04-18 14:01:38 -04:00
Gustavo Madeira Santana
4f9460f165 test(agents): cover canonical dm peer aliases 2026-04-18 14:01:38 -04:00
Gustavo Madeira Santana
9c5d8e3025 fix(agents): preserve prefixed Teams peer ids 2026-04-18 14:01:38 -04:00
Gustavo Madeira Santana
860689c685 fix(regression): preserve peerless bound account fallback 2026-04-18 14:01:38 -04:00
Gustavo Madeira Santana
d643e46666 routing: share peer-kind matching 2026-04-18 14:01:37 -04:00
Gustavo Madeira Santana
d1fe20fefc tests: simplify bound-account spawn cases 2026-04-18 14:01:37 -04:00
Gustavo Madeira Santana
17e6d699b1 agents: clarify spawn peer alias lookup 2026-04-18 14:01:37 -04:00
Gustavo Madeira Santana
6b1a3b02c0 agents: preserve canonical spawn peer ids 2026-04-18 14:01:37 -04:00
Gustavo Madeira Santana
61deb90e24 agents: share target-bound spawn origins 2026-04-18 14:01:37 -04:00
Gustavo Madeira Santana
f8d7b5dfc9 Agents: document target-bound subagent routing 2026-04-18 14:01:37 -04:00
Luke Boyett
09647e87a7 fix(agents): let id-embedded kind markers override prefix-derived kind
Matrix thread delivery encodes per-user DM targets as `room:@user:server`
— the `room:` wrapper says "channel" but the embedded `@` id marker
says "direct". The previous extractRequesterPeer gated the `@`/`!`/`#`
heuristic on `!inferredKind`, so the prefix-derived kind won and a
direct-kinded peer binding on the same user id was rejected by the
kind-safety check in resolveFirstBoundAccountId. Cross-agent spawns
whose target was bound as a direct Matrix peer could fall back to the
caller account and send from the wrong identity.

The fix removes the `!inferredKind` guard so id-embedded kind markers
always have the final say — they are a more reliable signal than the
delivery-target wrapper, because channel-side prefixes can wrap either
a room or a user id.

Regression test: sessions_spawn classifies Matrix `room:@user` targets
as direct, not channel — the lifecycle suite now configures a
conflicting `channel`-kinded binding on the same user id and asserts
the `direct`-kinded binding wins when the caller's `agentTo` is
`room:@user:example.org`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:01:36 -04:00
Luke Boyett
2efc727406 fix(agents): generic <word>: prefix peeler for delivery targets
MS Teams inbound context sets OriginatingTo to `conversation:<id>` while
route bindings key on the bare conversationId. The previous hand-rolled
prefix list (`room:`, `channel:`, `chat:`, `group:`, `user:`, `dm:`)
missed `conversation:` (and any future channel-specific namespacing), so
Teams cross-agent subagent spawns fell back to channel-only/caller
account and posted from the wrong identity.

extractRequesterPeer now uses a generic `^[a-z][a-z0-9_-]*:` regex to
peel any lowercase-alpha token-colon prefix, looping until the raw peer
id surfaces. Real-world peer ids never start with a lowercase-alpha
token followed by `:` (Matrix uses `!`/`@`, IRC `#`, Slack/Discord/LINE
alphanumerics, numeric Telegram/WhatsApp, or email-style `user@server`),
so this is safe. Known prefixes are mapped to ChatType for peerKind
inference (`conversation:`/`room:`/`channel:`/`chat:`/`thread:`/`topic:`
→ channel, `group:`/`team:` → group, `user:`/`dm:`/`pm:` → direct).

Regression test: sessions_spawn strips `conversation:` prefix for
Teams-style targets — a binding keyed on the raw conversation id
resolves correctly when the caller's `agentTo` is `conversation:<id>`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:01:36 -04:00
Luke Boyett
3824ee0d6f fix(routing): wildcard/peerless/kind-equivalence fixes for bound-account lookup
Addresses three further review findings:

1. Parse non-Matrix peer kinds before wildcard account lookup (P1)
   Channels like LINE emit `line:group:<id>` / `line:room:<id>` targets,
   but the Matrix-style heuristic only recognized `@`, `!`, and `#` so
   the kind was lost and wildcard bindings with a declared kind were
   skipped. subagent-spawn now uses a single `extractRequesterPeer`
   helper that tries the channel plugin first, then peels the channel
   namespace, then loops stripping kind-prefixes (`room:`/`channel:` →
   `channel`, `group:` → `group`, `user:`/`dm:` → `direct`, `chat:` →
   `channel`) — capturing the kind from the prefix as authoritative —
   before falling back to the id-only heuristic for Matrix/IRC shapes.

2. Allow wildcard bindings in peerless bound-account fallback (P2)
   Peerless callers (cron delivery resolution passes only
   `channelId`/`agentId`) were blocked by the strict wildcard kind
   safety, silently regressing configs that only declare wildcard peer
   bindings. Peerless callers now accept wildcard bindings as a last-
   resort fallback — the kind-safety rule only applies when the caller
   supplies a peer to verify against.

3. Treat `group` and `channel` peer kinds as equivalent (P1)
   Routing elsewhere (`peerKindMatches` in `src/routing/resolve-route.ts`)
   intentionally accepts group/channel as compatible so a binding
   declared as `peer.kind: "group"` resolves for callers inferred as
   `channel` (Matrix rooms, Mattermost/Slack channels) and vice versa.
   `resolveFirstBoundAccountId` now applies the same semantics in both
   the wildcard and exact-id kind-cross-check branches.

Regression coverage in src/routing/bound-account-read.test.ts:
- Treats group and channel peer kinds as equivalent (both directions).
- Accepts a wildcard peer binding as fallback for peerless callers.
- Skips wildcard peer bindings when the caller's peerKind is unknown.

Regression coverage in openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts:
- sessions_spawn peels channel prefix then kind prefix for
  `<channel>:<kind>:<id>` targets (LINE `line:group:<id>` shape) and
  correctly picks the `group`-kinded binding over a `direct`-kinded
  wildcard binding for the same agent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:01:36 -04:00
Luke Boyett
5ac04e4c0b fix(routing): strict wildcard kind safety + channel-prefix-first peer normalization
Addresses two new Codex P1s on the bound-account lookup path:

1. Strip channel prefix before generic target-kind prefixes
   (src/agents/subagent-spawn.ts)

   normalizeRequesterPeerId stripped a single generic prefix before the
   `<channel>:` namespace, so LINE delivery targets of the form
   `line:group:<id>` ended up as `group:<id>` instead of `<id>` and the
   exact peer-id binding was missed. The helper now peels the channel
   namespace first, then loops over generic prefixes (room:, channel:,
   chat:, user:, dm:, group:) until the raw peer id surfaces.

2. Enforce peer-kind safety for wildcard bindings when caller kind is
   unknown (src/routing/bound-account-read.ts)

   A `peer.id: "*"` binding with an incompatible kind (for example
   direct/*) could still be accepted when the caller did not supply a
   peerKind — channels whose plugin does not implement inferTargetChatType
   (such as Matrix) could then have room-originated spawns resolve to
   the wrong DM-bound account, which is worse than preserving the caller
   account. Wildcard matches now require both sides to declare a
   peer.kind and for the kinds to agree; otherwise the binding is
   skipped. Exact peer-id matches still require id equality, with a
   kind cross-check only when both sides declare a kind (peer ids are
   channel-unique).

   To preserve the existing Matrix-room happy path (the Matrix plugin
   does not expose inferTargetChatType), inferPeerKindForChannel gains a
   conservative fallback that recognizes `!`-prefixed room ids as
   "channel" and `@`-prefixed user ids as "direct", matching the
   existing `normalizeId` convention in extensions/matrix.

Regression coverage:

- src/routing/bound-account-read.test.ts:
  - Skips wildcard peer bindings when caller's peerKind is unknown.
  - Matches exact peer id even when caller's peerKind is unknown.
  - Updated prefers-wildcard-over-channel-only test to pass an explicit
    peerKind reflecting the new strict requirement.
- src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts:
  - sessions_spawn peels channel prefix then kind prefix for
    `<channel>:<kind>:<id>` targets — LINE-style `line:group:<id>`
    resolves to the binding configured on the raw peer id.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:01:36 -04:00
Luke Boyett
9cfb6e1d85 fix(agents): normalize requester peer id before bound-account lookup
Delivery targets on Matrix (and other channels that namespace the `to`
field) arrive in `kind:<id>` form — for example `room:!abc:example.org`
— while route bindings store the raw peer id on `match.peer.id`
(`!abc:example.org`). Passing `ctx.agentTo` directly to
`resolveFirstBoundAccountId` caused exact peer matches to silently fail
and the lookup to fall through to channel-only or caller-account
fallback, so cross-agent spawns could still post as the wrong identity
when only a peer-specific binding was configured.

- src/agents/subagent-spawn.ts: strip known delivery-target prefixes
  (`room:`, `channel:`, `chat:`, `user:`, `dm:`, `group:`, and the
  channel-namespaced `${channelId}:`) from `requesterTo` before handing
  it to `resolveFirstBoundAccountId`. The inferred `peerKind` still uses
  the original `requesterTo` so channel plugins can apply their own
  inference on the wire format.

Regression test in lifecycle suite:
- sessions_spawn strips channel-side prefixes from agentTo before the
  bound-account lookup — a binding on the raw room id resolves correctly
  even when the caller's `agentTo` is `room:<id>`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:01:36 -04:00
Luke Boyett
6faff0c343 fix(routing): match bound peers by kind before applying wildcard precedence
An agent with bindings that differed only by peer kind (for example
direct/* and channel/*, or the same peer id across kinds) could pick the
wrong sender account in resolveFirstBoundAccountId because the lookup
compared peer.id only and dropped peer.kind. Combined with the peerId
now always being forwarded from subagent spawns, an unrelated binding
could win purely by config order and route child messages from the
wrong identity.

- src/routing/bound-account-read.ts: preserve peer.kind in the
  normalized match and accept an optional peerKind on
  resolveFirstBoundAccountId. When both caller and binding declare a
  kind, they must match or the binding is skipped. If either side omits
  the kind, kind is not used as a filter (preserves prior behavior for
  callers that do not know the kind, such as cron delivery resolution).

- src/agents/subagent-spawn.ts: derive peerKind for the lookup via the
  active channel plugin's inferTargetChatType helper and pass it
  through. Same-agent spawns still short-circuit the lookup entirely.

Regression coverage in src/routing/bound-account-read.test.ts:

- Filters bindings by peer kind when caller supplies peerKind —
  direct/* and channel/* wildcards resolve to distinct accounts.
- Skips peer-specific bindings whose kind does not match the caller's
  peerKind, falling through to the channel-only binding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:01:36 -04:00
Luke Boyett
46708707f6 fix(routing): address three review P1s for bound-account lookup
1. Preserve requester account for same-agent subagent spawns
   (src/agents/subagent-spawn.ts)

   resolveRequesterOriginForChild previously overwrote the caller's active
   inbound accountId with a fresh binding lookup even when the target
   agent equaled the requester. In multi-account channels where the same
   agent has multiple bindings, that could switch the child to a
   different account from the one the parent is actively using. Now the
   lookup only runs when targetAgentId differs from requesterAgentId;
   same-agent spawns keep the caller's accountId unchanged.

2. Skip wildcard peer matches when peerId is absent
   (src/routing/bound-account-read.ts)

   A wildcard peer binding (peer.id: "*") represents "match any peer", so
   it is only meaningful when the caller supplies a peer. For peerless
   callers (e.g. cron delivery resolution) a wildcard binding no longer
   beats a channel-only binding. It is instead used as a last-resort
   peer-ish fallback (see #3 below).

3. Preserve old first-match semantics for peerless callers with only
   peer-specific bindings (src/routing/bound-account-read.ts)

   The previous iteration skipped peer-specific bindings outright when no
   peerId was supplied, which silently dropped the account for operators
   whose only binding was peer-specific (for example cron jobs targeting
   a specific room). Peerless callers now fall back to the first
   peer-specific or wildcard binding they find (peerlessPeerSpecificFallback)
   after channel-only bindings, restoring the prior cron semantics
   without overriding explicit channel-only configuration.

Final three-tier precedence summary:

- peerId supplied:
    exact peer match > wildcard peer > channel-only.
    Non-matching peer-specific bindings are skipped.
- peerId absent:
    channel-only > first peer-specific or wildcard binding found.

Regression coverage:

- src/routing/bound-account-read.test.ts (new): six unit tests covering
  exact peer match, wildcard-wins-over-channel-only for peer-present
  callers, channel-only-wins-over-wildcard for peerless callers,
  peer-specific fallback for peerless callers with no channel-only
  binding, skipping non-matching peer-specific bindings, and the
  agent-on-different-channel no-match case.

- src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts:
  new test asserting that a same-agent sessions_spawn (no explicit
  agentId) preserves the caller's accountId rather than re-resolving via
  bindings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:01:35 -04:00
Luke Boyett
de20d381bc fix(agents): prefer target agent's bound Matrix account for subagent spawns
A Matrix-bound parent session spawning a subagent for another agent could
seed the child's deliveryContext.accountId with the caller's account
before target bindings were consulted, causing child posts to come from
the wrong Matrix identity.

Changes:

- src/agents/subagent-spawn.ts: add resolveRequesterOriginForChild(...)
  that prefers the target agent's bound account via
  resolveFirstBoundAccountId({ cfg, channelId, agentId: targetAgentId,
  peerId: requesterTo }) before falling back to the caller's accountId.

- src/routing/bound-account-read.ts: resolveFirstBoundAccountId now
  accepts an optional peerId and uses three-tier precedence matching
  resolve-route.ts semantics:
    1. exact peer match wins immediately
    2. wildcard peer match (peer.id: "*") wins over channel-only
    3. channel-only binding is the final fallback
  The existing cron delivery-target caller is unaffected; it passes no
  peerId so it still gets channel-only matching.

Regression coverage in
openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts:

- Matrix room-bound route uses the target agent's bound account over the
  caller's account.
- Peer-specific binding wins over channel-only binding for the same
  agent.
- Non-matching peer falls back to the channel-only binding.
- Wildcard peer binding (peer.id: "*") matches any peer and beats
  channel-only binding.
- Exact peer binding wins over a wildcard peer binding for the same
  agent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 14:01:35 -04:00
Peter Steinberger
3f3bc97cd3 chore(lint): enable warning comments rule 2026-04-18 18:55:18 +01:00
Peter Steinberger
235cdb3f81 refactor: remove ollama core facades 2026-04-18 18:53:04 +01:00
Peter Steinberger
6b525023d4 fix: polish Slack thread starter context (#68594) 2026-04-18 18:45:29 +01:00
Peter Steinberger
5cc4426f88 test: align qa multipass pnpm expectation 2026-04-18 18:39:03 +01:00
Peter Steinberger
089e038dfe fix: harden macOS update restart helper (#68492) (thanks @hclsys) 2026-04-18 18:39:03 +01:00
HCL
4a870300dd fix(update-cli): capture macOS launchctl stderr to a log file instead of /dev/null
The macOS restart helper emitted by `openclaw update` (darwin branch of
`prepareRestartScript`) wrote the gateway restart script with every
`launchctl` stderr redirected to `/dev/null` and the final fallback
`kickstart` chained with `|| true`. When bootstrap/kickstart failed
(plist-on-disk race, schema rejection, stale job, bootout recovery
edge cases), the script exited 0, the updater declared success, and
the gateway silently stayed offline.

The reporter saw a ~25 minute production outage before noticing the
messages going unanswered across Telegram/Discord/Feishu.

Route stderr to `~/.openclaw/logs/update-restart.log` via `exec 2>>`,
drop `2>/dev/null` on every launchctl call, and remove the `|| true`
swallow on the fallback kickstart so a genuine failure exits non-zero
and leaves a durable audit trail. Log directory creation is best-effort
via `mkdir -p ... 2>/dev/null || true` since it normally already exists
from the gateway's own logging path. Self-cleanup of the script file
via `rm -f "$0"` is retained because the log, not the script, is the
useful artifact after the fact.

Adds a targeted regression test `captures macOS launchctl stderr to
~/.openclaw/logs/update-restart.log` alongside the existing darwin
restart-script test. The existing test's assertions about the
kickstart/enable/bootstrap fallback chain + self-cleanup all still pass.

Fixes #68486
2026-04-18 18:39:03 +01:00
Peter Steinberger
90c1ab2cef build: add tsgo profiler 2026-04-18 18:39:01 +01:00
Peter Steinberger
16bd427cb6 test: speed apply-patch and exec approval hotspots 2026-04-18 18:33:16 +01:00
Peter Steinberger
e45a50c828 perf: narrow subagent test runtime seams 2026-04-18 18:33:15 +01:00
Peter Steinberger
4180e7cd59 test: dedupe skills and model config coverage 2026-04-18 18:33:15 +01:00
Peter Steinberger
6d776593ea perf: lazy-load skills install extraction seams 2026-04-18 18:33:15 +01:00
Peter Steinberger
df525b90f2 chore(lint): enable unnecessary type parameter rule 2026-04-18 18:31:13 +01:00
Peter Steinberger
630f2bcabe fix: harden published gateway secret placeholders 2026-04-18 18:29:10 +01:00
Coy Geek
106b770c40 Gateway: reject published placeholder tokens 2026-04-18 18:29:10 +01:00
Coy Geek
960bc52e3c fix(install): remove published gateway token placeholder
Co-authored-by: opencode <opencode@users.noreply.github.com>
2026-04-18 18:29:10 +01:00
Peter Steinberger
1a7d89e85b docs: add WeChat channel guide 2026-04-18 18:26:40 +01:00
Peter Steinberger
3d994aa03b docs: clarify tsgo typecheck lanes 2026-04-18 18:24:07 +01:00
Peter Steinberger
72979129fb build: split tsgo core and extension graphs 2026-04-18 18:22:26 +01:00
Peter Steinberger
e11039087c build: add targeted tsgo test graphs 2026-04-18 18:12:44 +01:00
Peter Steinberger
cd2ef0f3a3 chore(lint): enable low-noise rules 2026-04-18 18:09:18 +01:00
Peter Steinberger
07785c6dbc build: split tsgo prod and test graphs 2026-04-18 18:06:29 +01:00
Peter Steinberger
753183e081 build(deps): update workspace dependencies 2026-04-18 18:04:56 +01:00
Peter Steinberger
c95d6049c2 chore(lint): preserve oxlint rule baseline 2026-04-18 18:04:56 +01:00
Peter Steinberger
76891c9cf8 fix: exclude ancestor pids from stale gateway cleanup (#68517) (thanks @openperf) 2026-04-18 18:03:55 +01:00
openperf
8aadca4c3e fix(infra/restart): exclude ancestor pids from stale-gateway cleanup
The stale-gateway cleanup filter already refused to kill process.pid —
acknowledging the invariant that terminating a process whose death
cascades into the caller is never safe. That invariant was applied only
to the caller itself, not to its ancestors, which is why the
openclaw-weixin sidecar triggered an unbounded restart loop: the
sidecar's cleanup SIGTERM'd its parent gateway, the supervisor
restarted the gateway, the gateway re-spawned the sidecar, the cleanup
ran again.

Complete the invariant by excluding the full self+ancestor PID set in
both the lsof (Unix) and PowerShell/netstat (Windows) cleanup paths.
Walk uses process.ppid unconditionally (Node built-in, no spawn) and
/proc/<pid>/status on Linux for transitive ancestors, with graceful
degradation where /proc is unavailable.
2026-04-18 18:03:55 +01:00
Peter Steinberger
aad9a833c0 fix: polish Slack thread fetch diagnostics (#68594) (thanks @martingarramon) 2026-04-18 17:55:05 +01:00
Martin Garramon
6368559c02 chore(scripts): bump slack media.ts fetch-allowlist line numbers
The `lint:tmp:no-raw-channel-fetch` allowlist pins exact line numbers
(scripts/check-no-raw-channel-fetch.mjs:63-65). The previous commit
added `import { logVerbose } from "openclaw/plugin-sdk/runtime-env";`
on line 8 of `extensions/slack/src/monitor/media.ts`, shifting the
three allowlisted raw `fetch()` callsites from 96/115/120 → 97/116/121.
Updates the allowlist to match the new positions. No behavior change —
the same callsites remain allowlisted.
2026-04-18 17:55:05 +01:00