* feat(onebot): support main-port reverse and forward WebSocket connections
* fix(onebot): keep protocol metadata out of business events
* test(onebot): validate WS events and APIs against independent protocol peer
---------
Co-authored-by: Hyu <[email protected]>
Core no longer assembles a whole trace locally. Every workflow node is uploaded
the moment it completes as its own `execution_node` record, and each execution
closes with exactly one `execution_chain` record; all records of one execution
carry the same `event_id`, so Space groups them back into a single chain.
- every execution starts at an inbound event: event-less Pipeline lanes and
WebUI/API debug runs synthesize their virtual inbound event at the boundary
and are marked debug=true, so a Runner is never a trace origin
- sampling is decided once per event from a stable hash of the event id (the
previous `int(id[:8], 16)` raised on every `platform:`-prefixed id and so
dropped every successful inbound-event chain) and is reported on each record
as sample{mode,denominator}; the default mode is now `all`
- the local trace buffer, the 120s TTL, the per-process trace cap and the shared
open-node mirror are gone, which also removes the truncated-chain upload and
the re-buffered-closed-trace leak they caused
- a broken chain is always uploaded, and its reason falls back to the first
failed node so the chain explains itself
- flush keeps records until the sender accepts them and counts what it drops;
telemetry health replaces the unused send-task queue
- heartbeat reports bound Agent / plugin-processor / event-binding counts
- bound Pipeline and Agent resources (plugins, MCP servers, knowledge bases,
tools with their Host source, skills, models and the Runner schema selectors
that picked them) are reported on the chain record
- failure text is bounded and redacted instead of passing exception text through
- the dead per-query feature counters and their call sites are removed
Wire contract: schema 2 (`execution_node` / `execution_chain`).
The Lark/Feishu webhook handler (both the current lark-omni adapter and
the legacy lark adapter) parsed and dispatched inbound HTTP callback
events without checking Lark's own X-Lark-Signature header, and the
Encrypt Key config field used to decrypt payloads is optional and empty
by default. Any caller who reached a bot's webhook URL could inject
fabricated message/card-action events with an attacker-chosen sender,
chat, and content.
Add signature verification matching Lark's documented
event-security-verification scheme (sha256 of timestamp + nonce +
encrypt_key + body, compared to X-Lark-Signature) to both
implementations, rejecting any callback that lacks a valid signature or
whose bot has no Encrypt Key configured. Adds regression tests covering
the rejection paths and updates the two existing webhook tests to
present a valid signature.
The execution trace rewrite batches records and uploads them from
ExecutionCounters.flush(), replaced the per-trace direct send, dropped the
window counters, made the execution id the record identity and records the
inbound event stage after the routing stage. The ingress tests still asserted
the old mechanism, so they failed on every Python version.
Assert what the pipeline now emits instead: flush the buffered records and read
them back, require the trace registry to be empty afterwards, assert the
deterministic execution id, expect the route stage before the inbound stage,
and give the dispatch stubs the execution_id parameter the ingress passes.
Execution telemetry already reported window counters. This adds one bounded,
content-free chain per inbound platform event, so a single event can be
followed from its source platform through routing and processing to the
platform API calls it caused.
- telemetry/trace.py: ContextVar trace identity with route and run scopes, and
a 32-stage bound per chain.
- telemetry/execution.py: keeps at most 64 chains for 120s, decides sampling
from the observed outcome, and sends one payload per chain keyed by
query_id = trace_id, so Space fetches a chain by primary identity. Window
counters are unchanged and remain the source of coverage statistics.
- botmgr / pipelinemgr / orchestrator: bind the trace at the ingress boundary,
scope routing identity per dispatch, and attach the run identity, so nested
lanes (event -> route -> pipeline/runner -> platform API) reuse one chain. A
run reached without an ingress (WebUI debug, service API) owns its own chain.
- space.execution_trace selects off | failures | sampled | all (default
sampled: failures, WebUI debug runs and every N-th success).
Chains carry only code-defined identifiers (adapter and runner types, route
identity as type:uuid, run id); never message content, tool arguments or
platform user identifiers.
* fix(pipeline): stop emitting empty final assistant messages
An empty final streaming chunk was appended as an empty message chain when no sandbox outbox attachment was present, so platforms received an empty reply. Only append the chain when attachments were actually collected, and gate the "Call ..." tool notice behind output.misc.track-function-calls so it no longer becomes the final chunk of a stream.
* fix(wecombot): do not close stream on a blank final snapshot
A blank final snapshot closed the WeCom stream with an empty bubble and pushed the real answer into a separate reply_text message. Skip blank final snapshots and keep the session open so the following non-blank chunk can finalize it. Covered by new regression tests.
WecomMessageConverter.yiri2target() always emits {'media_id': ...} for
image/voice/file parts (never 'media'), matching the correct usage
already in reply_message(). send_message(), the entry point plugins
use via PluginToRuntimeAction.SEND_MESSAGE, instead read
content['media'], which is never set, so any image/voice/file part
raises KeyError and aborts the send.
Refs #1687
Signed-off-by: Amir Fathi <[email protected]>
The LINE adapter passed text through as a single Plain component,
ignoring the mention payload (mentions[].index/length/isSelf) that the
Line Messaging API includes in the webhook. As a result:
- At(target=bot_account_id) never appeared in the message chain, so the
'at-bot' group respond rule silently dropped every @bot mention.
- The bot only replied when the message happened to match the prefix
rule (e.g. starting with 'ai').
Now LINEMessageConverter reads message.message.mention and builds the
chain per mention position:
- Bot mention (isSelf) -> At(target=bot_account_id) so AtBotRule matches
the same way as other adapters (dingtalk/lark etc.).
- Other mentions -> At(target=<line user id>, display=<mention text>).
At.__str__ already prepends '@', so the display text carries no
double '@' and the rendered text (prefix/regexp rules, quotes,
session context) is byte-identical to before.
- Missing/out-of-bounds mentions are skipped defensively.
target2yiri becomes an instance method (like wechatpad/aiocqhttp) so
the converters can hold bot_account_id; LINEAdapter passes it in from
its own config.