* fix(diagnostics-otel): surface error message on run/harness error spans
Errored openclaw.run / openclaw.harness.run spans only carried a
low-cardinality errorCategory (or a hardcoded "error"), so trace UIs
showed "outcome error" with no message. Thread the redacted error
message through run.completed / harness.run.completed / harness.run.error
onto an openclaw.error span attribute + span status, mirroring
recordWebhookError. The raw message stays off metric attrs to preserve
cardinality; support-bundle redaction covers the new error field by name.
* fix(diagnostics-otel): harden run failure telemetry
Co-authored-by: Alex Knight <aknight@atlassian.com>
* test(diagnostics-otel): assert wire-level redaction
* chore(changelog): defer release note to release process
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(codex): project guardianWarning circuit-breaker notification
Codex emits a `guardianWarning` notification from its rejection
circuit-breaker (e.g. 3 consecutive or 10 total denials in a turn)
right before ending the turn as interrupted. The app-server event
projector had no case for it, so the rejection-limit reason was
silently dropped and the turn degraded to a generic interruption.
Project it on the `codex_app_server.guardian` stream with phase
"warning", alongside the existing autoApprovalReview handling. The
exact v2 param shape is not bundled in OpenClaw's protocol schemas,
so the human-readable reason is read defensively.
Closes#101207
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(codex): route guardianWarning through thread-scoped filter
`guardianWarning` is thread-scoped — codex emits it with only
{message, threadId} (no turnId). The turn-strict pre-switch filter
therefore dropped it before it reached the projection switch, making
the handler added in the previous commit dead code for real traffic.
Correlate it on the thread alone (mirroring hook notifications) so it
reaches the switch, and read the `message` field per the codex schema.
The regression test now sends the real {threadId, message} shape
instead of a synthetic turnId, so it actually exercises the routing.
Refs #101207
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* repro(codex): add real behavior proof for guardianWarning projection
* fix(codex): narrow guardian warning projection
Co-authored-by: 曾文锋0668000834 <zeng.wenfeng@xydigit.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(imessage): handle stdout/stderr stream errors in the RPC client child
A dead imsg RPC helper can emit an async error on any of its stdio streams.
On a raw stream an unhandled 'error' event throws and surfaces as an
uncaughtException, crashing the gateway. #75438 added this guard for stdin
but left stdout/stderr — on the same long-lived child — unguarded.
Route stdout/stderr stream errors through the existing failAll path via a
shared failFromStreamError helper, mirroring the stdin handler. Add a
regression test that emits 'error' on each stream and asserts the child does
not throw and in-flight requests reject cleanly.
* fix(imessage): terminate failed RPC transports
* test(imessage): exercise real stream failure
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(qa): canonicalize channel routing scenarios
* test(qa): enforce Crabline actor allowlists
* test(qa): preserve canonical flow integration
* test(matrix): use retained topology fixture id
* test(qa): preserve WhatsApp live defaults
* QA: declare channel transport policy in scenarios
* refactor(qa): keep transport policy type local
* refactor(qa): keep transport policy on leaf contract
* test(matrix): use retained routing fixture
* fix(memory-core): clamp widen-fallback kNN k to sqlite-vec 4096 limit
The widen fallback in searchVector re-runs the kNN with k = vectorCount
(the full vector table size), unclamped. sqlite-vec hard-caps `k` at 4096,
so once a memory index exceeds 4096 chunks any query that triggers the
fallback fails with "k value in knn query too large".
Clamp the widen re-query to MAX_VECTOR_KNN_K (4096). 4096 oversampled
candidates is far more than enough to feed top-N hybrid ranking, so there
is no meaningful recall loss.
* test(memory-core): cover widen-fallback kNN clamp to sqlite-vec 4096 ceiling
Add two regression tests for the searchVector widen-fallback clamp:
- asserts a >4096-vector widen re-query caps k at 4096 (out-of-range k
would raise sqlite-vec "k out of range" and kill the whole search)
- asserts a sub-ceiling widen (30) passes through untouched, proving the
clamp is a ceiling and not a fixed value
Uses a prepare-mock on the KNN/COUNT statements rather than materializing
4097+ real vectors plus the native extension; the assertion is purely on
the k bind handed to the widen re-query.
* fix(memory-core): preserve filtered recall beyond KNN cap
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(openai): bound Codex OAuth token response body reads with readResponseWithLimit
Replace unbounded response.arrayBuffer() in postTokenForm with
readResponseWithLimit using a 1 MiB cap to prevent OOM from oversized
token endpoint responses. Add real node:http loopback server tests.
* fix(openai): wrap readResponseWithLimit result in Uint8Array for TS BodyInit compat
- Fixes TS2345: Buffer<ArrayBufferLike> not assignable to BodyInit
- Resolves check-prod-types, check-test-types, and
check-additional-extension-package-boundary CI failures
Ref. https://github.com/openclaw/openclaw/pull/99479
* test(openai): verify OAuth response release
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): drain queued outbound deliveries after gateway reconnect (#56610)
Discord sends that failed while the gateway websocket was reconnecting
(close codes 1005/1006) stayed stuck in the outbound delivery queue and
were silently dropped. Classify sends that fail while the registered
gateway is disconnected as retryable, and drain pending discord queue
entries when the gateway reconnects, matching the WhatsApp/Telegram
reconnect drains.
* fix(discord): harden reconnect recovery
Co-authored-by: tiffanychum <71036662+tiffanychum@users.noreply.github.com>
* refactor(discord): remove unsafe reconnect replay
* fix(discord): bound reconnect retry per request
Co-authored-by: tiffanychum <71036662+tiffanychum@users.noreply.github.com>
* chore(changelog): leave reconnect fix to release flow
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): download attachments at receipt time, not after the run queue
Discord's CDN attachment URLs carry an expiring `ex` TTL. Media was
downloaded in processDiscordMessageInner, after the inbound run queue,
so messages delayed behind a busy run lost their attachments silently.
Resolve attachments/forwarded media during preflightDiscordMessage
instead, before the message is enqueued, and carry the result forward
on the context for process to reuse.
Fixes#96165
* refactor(discord): carry one prepared media snapshot
Co-authored-by: ZacharyYW <zachary.w.yuan123@gmail.com>
* fix(discord): guard bot media before download
* chore(discord): leave release notes to release flow
* docs(changelog): credit Discord attachment fix
* chore(changelog): leave attachment fix to release flow
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): bound gateway metadata response body reads to prevent OOM
The materializeGuardedResponse helper in the Discord gateway metadata path
buffered the full upstream Response body via response.arrayBuffer() without
any size cap. A malicious or malfunctioning /gateway/bot endpoint that returns
an oversized payload could exhaust gateway memory.
Replace arrayBuffer() with readResponseWithLimit(4 MiB), consistent with
DISCORD_API_RESPONSE_BODY_LIMIT_BYTES in api.ts. Overflow throws an Error
with the byte counts.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(discord): wrap readResponseWithLimit result in Uint8Array for type compat
readResponseWithLimit returns Buffer which is not assignable to BodyInit in
the undici Response constructor type. Wrap in new Uint8Array() to satisfy the
boundary dts check. Also remove testExports export and mock fetchWithSsrFGuard
in tests via vi.hoisted + vi.mock to avoid leaking internal test-only exports.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(discord): tighten gateway metadata proof
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(device-pair): remove INADDR_ANY and IPv6 unspecified from isLoopbackHost
0.0.0.0 (INADDR_ANY — bind to all interfaces) and :: (IPv6 unspecified)
are NOT loopback addresses. The canonical implementation in
src/gateway/net.ts explicitly excludes both. Classifying them as
loopback in isLoopbackHost causes incorrect behavior in mobile
pairing cleartext host validation.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(device-pair): reject unspecified setup URLs
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(browser): guard readFields JSON.parse against malformed user input
Wraps JSON.parse(payload) in readFields() with try/catch, throwing a
descriptive Error when CLI --fields or --fields-file input contains
malformed JSON instead of raw SyntaxError.
8/8 proof assertions pass using standalone script that imports and
calls the real exported readFields() with malformed field strings.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
* fix(browser): add vitest malformed JSON tests, remove standalone proof
ClawSweeper feedback: added 2 vitest tests (malformed JSON, empty fields)
to the existing shared.test.ts, removed standalone proof script.
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
* fix(browser): cover malformed fields at CLI boundary
---------
Signed-off-by: lsr911 <liao.shirong@xydigit.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): use configured statusReactions.timing instead of DEFAULT_TIMING
Makes the Discord message finalizer respect user-configured
doneHoldMs/errorHoldMs instead of always using hardcoded
DEFAULT_TIMING values. Brings Discord to parity with Telegram
and Slack which already use configured timing.
Fixes#78431
* test(discord): strengthen configured-timing regression with old-default assertion
Add a no-cleanup assertion at DEFAULT_TIMING.doneHoldMs (1500ms) before
advancing to the configured 2000ms hold, so the test fails against the
old hardcoded-default behavior. Add a matching errorHoldMs regression
test using failedCounts to drive the error terminal path.
Address ClawSweeper [P2] review finding on #94736: the previous test
advanced straight to 2000ms and would pass without the runtime fix
because main already removes the done reaction at 1500ms.
* refactor(discord): unify status reaction timing proof
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): handle ffmpeg stderr stream errors in voice playback
createDiscordOpusPlaybackStream guards ffmpeg stdout and stdin against raw
stream 'error' events (an unhandled stream error throws and crashes the
gateway via uncaughtException), but the stderr stream on the same child was
left unguarded. Add the symmetric stderr error handler, routing it through
opusStream.destroy like stdout. Add a regression test covering stdout and
stderr stream errors.
* refactor(discord): unify ffmpeg stream errors
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(memory-core): clear daily-ingestion sqlite namespace on dreaming repair
repairDreamingArtifacts() cleared the dreaming-session-ingestion-files and
dreaming-session-ingestion-seen namespaces but not the migrated
dreaming-daily-ingestion namespace. After #92020 taught auditDreamingArtifacts()
to treat all three ingestion namespaces as ingestion state, the repair path
became asymmetric: the memory status --fix re-audit still reported
sessionIngestionExists=true from the surviving daily rows, and the daily
ingestion bookkeeping leaked past repair so daily memory files were not
re-ingested on the next sweep.
Clear DREAMING_DAILY_INGESTION_NAMESPACE alongside the session files/seen
namespaces so repair fully resets dreaming ingestion state, matching the audit.
* chore: retrigger CI for real behavior proof check
* fix(memory-core): keep daily ingestion outside session repair
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Co-authored-by: Alix-007 <li.long15@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>