The built-in default footer (added in #92657) only defined a `discord`
surface. Telegram and every other surface fall through to `output.default`,
which was authored for a wide terminal: space-separated badges, a leading
space-less join onto the message body, and ↕️ token + 🗄 cache% segments.
On a narrow Telegram screen this wrapped at arbitrary spaces (often mid-meter
or between 📚 and its bar) and welded onto the end of the message text.
Make `default` read well on a phone:
- Leading \n so the footer sits on its own line below the body.
- Tight badges (no separators between model/flags/reasoning/fast).
- NBSP before "|" and 💰; 📚 glued hard to the meter so it can't wrap there.
- Single regular space after "|" as the only sanctioned wrap point.
- Drop ↕️ tokens and 🗄 cache% from the default — noise for most users
(still available to anyone via a custom messages.usageTemplate).
Discord surface trimmed to match for consistency (keeps its -# dim header).
ClawSweeper review flagged that the new suppressReply test replaced the
existing direct-message binding regression test instead of being added
beside it. Restore the DM binding test to maintain coverage for the
plugin command dispatch surface.
Adds to the type so plugin
commands that handle their own transport delivery (e.g. via Telegram Bot API
directly with retry logic, IPv4 forcing, etc.) can signal the channel adapter
to skip the fallback reply.
When a plugin command handler returns , the Telegram
native command dispatcher now:
1. Cleans up any progress placeholder
2. Returns early without sending the "No response generated. Please try again." fallback
Includes detailed JSDoc for the new flag explaining its use
for plugin commands that deliver their own responses via channel-native APIs.
Fixes#80756
* fix(embedded-agent-runner): pump async streamFn through pumpStreamWithRecovery for mid-stream error recovery
When wrapEmbeddedAgentStreamFn returns an async function (e.g. when
authStorage or resolvedApiKey is present), the stream is a Promise that
resolves to AssistantMessageEventStreamLike. The old Promise branch in
wrapAnthropicStreamWithRecovery only handled Promise rejections via
.catch(), missing {type:"error"} events that arrive after the Promise
resolves — such as Anthropic thinking-signature replay rejections.
Now the Promise branch:
- Awaits the resolved stream and runs pumpStreamWithRecovery on it,
so mid-stream error events trigger thinking-signature recovery.
- Handles Promise rejection via .then(onFulfilled, onRejected) with
the same retryStreamWithoutThinking path used by the sync branch.
- Returns a unified AssistantMessageEventStream (outer) identical to
the non-Promise branch pattern.
Tests updated to match the new return type (outer stream + .result()
instead of a bare Promise).
Fixes#95429
* fix(anthropic): remove orphaned wrapRetryStreamWithRecoveryNotification
Function is dead code — the Promise-handling was inlined into
pumpStreamWithRecovery, leaving only a recursive self-call
with no external entry point.
* fix(embedded-agent-runner): add Promise-resolved-stream regression test
* fix: use createTestStreamErrorMessage for type-correct stream error in Promise-resolved test
* test(agents): cover async thinking stream recovery
---------
Co-authored-by: lzyyzznl <lzyyzznl@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
isCodexToolResultError fail-closes any dynamic-tool result whose
details.status is absent from its non-error allowlist. get_goal returns
details.status "found" or "missing" (a successful read of the thread
goal, or its absence), neither of which was in the allowlist, so every
get_goal call was classified as an error: reported to codex as
success: false and persisted on the transcript with isError: true.
Sibling #96856 added the write-side goal statuses (created/updated) and
the accepted spawn status but missed the read-side get_goal statuses.
This adds found/missing alongside them. Genuinely failed statuses stay
fail-closed.
* fix(memory-wiki): disambiguate the reserved index page stem for synthesis and ingest
A create_synthesis or source ingest whose title slugifies to index was
written to the auto-generated <dir>/index.md. That file is the only
compiler-owned page filename: compile writes it per page group, and
compile/query/status exclude it from page scanning. So the stored fact
was both unretrievable and clobbered by the generated directory-index on
the next compile.
The collision only happens at the direct page-stem boundary: synthesis
(syntheses/<slug>.md) and ingest (sources/<slug>.md) build a bare
<dir>/<slug>.md path. The bridge, unsafe-local, and OKF callers compose
slugifyWikiSegment output into prefixed and hashed identities that can
never equal a bare index.md, and OKF already guards its own write path
via OKF_RESERVED_FILENAMES.
Add a dedicated slugifyWikiPageStem helper that escapes the reserved
index stem and route only the two direct page writers through it,
leaving slugifyWikiSegment output stable for the composed callers so
their persisted page ids and paths do not churn on upgrade. Only index
is reserved: the compiler never owns <dir>/log.md in these directories
(the wiki log lives at .openclaw-wiki/log.jsonl), so a Log title keeps
its existing direct path. Additive and deterministic, no migration.
* fix(memory-wiki): preserve reserved-page ids
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(whatsapp): thread authDir through command authorization and owner bypass for LID JID resolution
WhatsApp group commands (/new, /stop) can be ignored when Baileys reports the sender as a LID JID (@lid) instead of a phone JID (@s.whatsapp.net). The resolveWhatsAppCommandAuthorized() and isOwnerSender() functions called getSelfIdentity/getSenderIdentity without passing authDir, so the LID-to-phone reverse mapping could not happen.
Fix: thread account.authDir through both command authorization and group owner-bypass identity resolution paths so that LID JIDs are properly resolved to phone E.164 identities before owner/allowlist checks.
* fix(whatsapp): replace deprecated top-level fields with admission overrides in LID JID test
* fix(feishu): send blocks as independent messages when blockStreaming is enabled
* fix(feishu): preserve mention targets on first independently sent block
* fix(feishu): route independent block sends through chunked text delivery
Route the independent block-send path through sendChunkedTextReply
instead of calling sendMessageFeishu directly, so the configured
Feishu textChunkLimit is honored for completed blocks. Preserve
mention targets only on the first emitted block chunk and keep
sentBlockText based on the original block text for duplicate-final
suppression.
Add regression test with a small textChunkLimit that verifies a
long block is split into chunked messages and mentions only appear
on the first chunk.
* fix(feishu): preserve block delivery target
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Claude Code >= 2.1.156 tightened its PermissionResult schema so an
approved can_use_tool control_response allow branch must carry an
updatedInput record. The Claude live-session bridge answered with the
deprecated { behavior: "allow" } shape and no updatedInput, so the CLI
rejected every approval-gated native tool call (Bash, WebFetch, and
AskUserQuestion among them) with a ZodError before the tool ran.
Echo the tool input back unchanged as updatedInput on the allow branch,
matching the shape the Claude Code 2.1 binary expects. The deny branch
and all other behavior are unchanged.
Fixes#95171.
hasProviderAuthForTool/hasAuthForProvider now thread cfg and workspaceDir into resolveEnvApiKey, so provider plugins that authenticate via environment variables and are only discoverable with config are detected during image/video model auto-selection instead of being dropped as unconfigured.
* fix(cron): persist startup catch-up deferral ids in service state to prevent read-RPC clobber
The startup overflow catch-up deferral (#93810) set deferred job
nextRunAtMs to a staggered slot, but the exemption was stored as a
local skipFutureRepairJobIds set threaded only into start()'s
maintenance recompute pass. Every other caller of
recomputeNextRunsForMaintenance — ensureLoadedForRead (list/status),
finalizeCompletedResults, empty-due tick, manual run preflight, and
releaseUnclaimedDueJobReservations — called maintenance recompute
without the skip set, causing shouldRepairFutureCronNextRunAtMs to
advance the deferred staggered slot to the job's natural next run,
dropping the missed run for a full period.
Fix: Move the exemption into CronServiceState as
pendingCatchupDeferralJobIds, populated by applyStartupCatchupOutcomes
where deferrals are assigned. recomputeNextRunsForMaintenance now
always checks this state-level set instead of a local parameter, and
clears each id once its staggered slot is reached (now >= nextRunAtMs).
This gives one canonical exemption path that covers all recompute
callers.
The now-redundant skipFutureRepairJobIds parameter is removed.
Fixes#93935
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(clownfish): address review for repair-94022-fresh-plan-20260618 (1)
* fix(cron): preserve agentTurn exempt + add stale-deferral cleanup + runtime proof
Address ClawSweeper review by:
1. Keeping the existing agentTurn deferred-slot exemption unchanged
(shouldRepairFutureCronNextRunAtMs still returns false for agentTurn
jobs with nextRunAtMs before the natural schedule).
2. Adding cleanup of stale pendingCatchupDeferralJobIds markers for
jobs that no longer exist or are disabled.
3. Adding regression tests for pending startup catch-up deferral
preservation and stale marker cleanup.
4. Adding standalone CLI runtime proof script demonstrating the fix.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: add pendingCatchupDeferralJobIds to test-harness + remove unused import
- Add pendingCatchupDeferralJobIds to createMockCronStateForJobs to fix TS2741.
- Remove unused loadCronStore import from runtime proof script.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(cron): tighten startup catch-up deferral handling
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(gateway): cap agentRunCache to prevent unbounded growth under run fan-out
Time-based prune only reclaims entries past the 10-minute TTL window; a burst
of run fan-out can add far more entries than the window reclaims, so the cache
could grow without bound between prunes. Add a FIFO entry cap (5000) enforced
on insert, mirroring the existing Discord REST entity-cache bound.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(gateway): preserve waited run snapshots under cache cap
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(tlon): bound error response body reads to prevent OOM
Replace bare response.text() on non-ok paths with readResponseTextLimited
capped at 16 KiB so a hostile or misconfigured Urbit ship cannot force the
gateway to buffer an arbitrary-size error body into process memory.
Affected paths:
- pokeUrbitChannel (channel-ops.ts)
- channel.runtime.ts poke path
- sendSubscription (sse-client.ts)
* fix(tlon): fix lint issues in error-body-boundary test
- Remove unused beforeEach import
- Wrap if/else bodies in braces (curly)
- Use block body for Promise executors (no-promise-executor-return)
* fix(types): resolve pre-existing TS test type errors
- Fix TS2493 tuple type errors in server-cron-notifications and
server-cron tests by adding explicit type annotations on mock.calls
- Fix TS2322 in anthropic.test.ts by adding as const to resource
content block type
* chore: trigger CI
* fix(update-check): bound npm registry JSON response read to prevent OOM
- Replace unbounded res.json() with readProviderJsonResponse capped at 16 MiB
- Update test mock to use real Response instead of bare object
- Verified against live npm registry: openclaw update status --json returns latestVersion: 2026.6.11
* fix(update-check): add 4 boundary tests for bounded npm registry read
- Tests: oversized response >16 MiB, near-boundary success
- Tests: malformed JSON, non-200 status code
- All pass with the readProviderJsonResponse bounded reader
* chore: add real behavior proof script for bounded JSON reader
Adds a standalone proof script that starts a local HTTP server and
drives the bounded reader against it with real HTTP requests, showing:
- Normal responses (~1 MB) parse correctly
- Oversized responses (>20 MB) are rejected with a descriptive error
- Malformed JSON is caught
- RSS does not spike on oversized responses
Ref. https://github.com/openclaw/openclaw/pull/98508
* chore: remove PR-specific proof script before merge
The proof script was accepted by ClawSweeper (proof: sufficient,
rating: diamond lobster), but it duplicates repository logic and
fails the scripts lint lane. Removing it per review feedback.
The real behavior proof remains in the PR body.
Ref. https://github.com/openclaw/openclaw/pull/98508
* fix(cli): retry logs.tail after journal fallback in logs follow
Rebase #88159 onto current main and keep systemd journal fallback temporary in follow mode. Preserve the journal cursor across repeated fallback outages, but retry logs.tail on the next loop so recovered Gateway RPC returns to normal log output.
This refresh also replaces the stale red checks-node-core-fast result from the old head with a current-head CI run.
* Keep log source metadata explicit
* ci: retrigger checks for PR #88159
* docs: clarify logs follow JSON source transitions
* fix(cli): keep journal logs responsive during recovery
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(status): bound systemd service probes so status cannot hang on a wedged systemctl (#84698)
* test(daemon): assert readRuntime receives the threaded status timeout opts (#84698)
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(cron): preserve provider/model on isolated-run timeout row
* fix(cron): carry provider/model/session into timer-built timeout rows
The reporter's blank cron_run_logs row (cron: job execution timed out
(last phase: ...)) comes from the wall-clock/cancel outcome built in
executeJobCoreWithTimeout, not the inner run catch the prior commit
patched (that catch result loses the Promise.race). Carry the
already-resolved attribution from watchdog-visible execution state into
the timer-built timeout and post-runner cancel outcomes so the persisted
row keeps provider/model/session. Pre-runner setup timeouts stay blank.
Refs #95873
* fix(cron): attribute timeout-disabled isolated-run operator-cancel rows
The timeoutSeconds:0 branch in executeJobCoreWithTimeout has no watchdog,
so it returned createOperatorCancellationOutcome() with no execution data
and never wired the execution callbacks. An operator-cancelled isolated run
with timeouts disabled still wrote a blank provider/model/session row.
Track the resolved identity locally from the same onExecutionStarted/
onExecutionPhase callbacks and carry it into the cancel outcome, matching
the timed branch. Adds a focused regression with negative control (#95873).
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary:
- The PR adds `cursor-agent` as an opt-in autoreview engine, updates the helper docs, runner dispatch, stream filtering, report parsing, and harness allowlists.
- PR surface: Docs +1, Other +99. Total +100 across 4 files.
- Reproducibility: not applicable. this is a feature PR, not a reported bug. The relevant check is source review plus maintainer validation of the external engine path.
Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head 807659492b.
- Required merge gates passed before the squash merge.
Prepared head SHA: 807659492b
Review: https://github.com/openclaw/openclaw/pull/97348#issuecomment-4824183439
Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Approved-by: hxy91819