Summary:
- The branch adds `useAutoCleanupTempDirTracker()`, broadens the temp-dir warning reporter to flag new manual helper imports/usages, updates docs, and migrates two script tests to the new helper.
- PR surface: Tests +301, Docs +1, Other +248. Total +550 across 8 files.
- Reproducibility: not applicable. this is test/tooling cleanup, and the changed behavior is exercised through helper/reporter tests and CI evidence rather than a user reproduction path.
Automerge notes:
- PR branch already contained follow-up commit before automerge: test: harden temp dir helper guard
- PR branch already contained follow-up commit before automerge: test: clarify auto cleanup temp dir helper name
- PR branch already contained follow-up commit before automerge: test: cover existing mkdtemp temp dir forms
- PR branch already contained follow-up commit before automerge: test: read staged temp helper source from index
Validation:
- ClawSweeper review passed for head 1fdd7d2a9a.
- Required merge gates passed before the squash merge.
Prepared head SHA: 1fdd7d2a9a
Review: https://github.com/openclaw/openclaw/pull/93209#issuecomment-4705653665
Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Approved-by: hxy91819
* fix(agents): don't inject A2A turns into isolated-cron sessions_send (#92257)
Fire-and-forget sessions_send (timeoutSeconds === 0) with announce
delivery runs the A2A ping-pong loop. For a cross-session send
(requester != target) the loop's first iteration feeds the target
agent's reply back into the requester session as a new turn. For a
normal requester that roundtrip is intended, but for an *isolated cron*
requester it injects reply context into the isolated run and causes an
agent feedback loop.
Narrow the fix to isolated-cron requesters only (detected by a session
key containing ":cron:" or channel "cron" -- the same signal used by
src/agents/subagent-registry.ts and set in
src/cron/isolated-agent/run.ts), NOT by timeoutSeconds. Gating on
timeoutSeconds was too broad: it disabled the intended ping-pong for
normal cross-session fire-and-forget sends.
Two hunks in src/agents/tools/sessions-send-tool.ts, both reusing one
`isIsolatedCronRequester` gate:
1. Force maxPingPongTurns to 0 in the runSessionsSendA2AFlow invocation
only for an isolated-cron requester. The a2a flow's ping-pong guard
(`maxPingPongTurns > 0`) then skips the requester-injection loop and
proceeds straight to the announce step in the TARGET session,
preserving fire-and-forget announce delivery. Normal requesters keep
the configured turn count.
2. Read baselineReply for fire-and-forget sends when same-session (prior
behavior) OR isolated-cron. Without a baseline fingerprint, a2a.ts
would treat pre-existing assistant text in the target session (e.g.
an unrelated concurrent cron's output) as "the reply" and
misattribute it. The normal cross-session fire leg's history-call
count is unchanged from origin/main. Read failures are tolerated so a
snapshot error never blocks accepting the send.
Tests (sessions.test.ts): an isolated-cron cross-session fire-and-forget
forwards maxPingPongTurns: 0; a normal-requester regression guard
(discord:group:req / channel discord) forwards the configured turn count
(not 0), proving the normal ping-pong is preserved. a2a flow tests
(sessions-send-tool.a2a.test.ts): with turns=0 + requester!=target the
requester is never stepped but the target is still announced, and a
baseline-matching reply is neither injected nor announced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(agents): gate isolated-cron A2A on canonical cron-run classifier (#92257)
Address Codex [P2] review: replace the raw `:cron:` substring detector with
the canonical isCronRunSessionKey so a non-canonical cron-like requester key
(e.g. agent:main:slack:cron:job:run:uuid) keeps its intended cross-session
ping-pong. Keep the requesterChannel === "cron" arm (isolated cron runs set
that channel in src/cron/isolated-agent/run.ts). Add a regression covering the
non-canonical key.
* refactor(agents): drop dead cron-channel A2A arm, gate on canonical key only (#92257)
The requesterChannel === "cron" arm was unreachable: agentChannel is always a
DeliverableMessageChannel from resolveGatewayMessageChannel(messageProvider),
never the literal "cron". The channel: "cron" in src/cron/isolated-agent/run.ts
labels diagnostics events/lifecycle, not the tool channel. Gate on
isCronRunSessionKey alone and fix the misleading comment.
Tests drove the dead arm via agentChannel/requesterChannel "cron" plus a
non-canonical key (agent:main:cron:run:abc, which isCronRunSessionKey rejects).
Switch them to a canonical cron-run key (agent:main:cron:job:run:abc) and a
normal delivery channel so they exercise the real production gate.
* fix(agents): align cron A2A fallback baselines
* chore: prepare branch refresh
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(gateway): distinguish reachable gateway from failed status probe
* fix(status): gate owns-port RPC recovery guidance on no stale gateway PIDs
inspectGatewayRestart can set health.healthy from bare reachability after
ownership attribution failed, while still returning a non-empty
staleGatewayPids. Printing owns-port recovery guidance in that case
contradicted the dedicated stale-PID diagnostic below it. Require
staleGatewayPids to be empty before treating healthy as owns-port proof.
* feat(cli): openclaw attach — launch Claude Code bound to a gateway session with scoped MCP tools
* fix(cli): use token-only MCP config for attach
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* Diagnose Windows LAN Gateway firewall blocks
* Fix Windows firewall diagnostic lint
* fix: gate gateway firewall diagnostics to local targets
* fix: keep firewall inspection off critical flows
Review on #98102: the durable-retry classifier treated every MediaFetchError
`fetch_failed` as retryable, but `fetch_failed` also covers permanent failures
(SSRF/guard denials, local Bot API path/read errors). On spooled replay those
would requeue forever with the user-facing warning suppressed.
Move the policy to its owner: add `isDurablyRetryableMediaFetchError` to
`src/media/fetch.ts`, defined as `shouldRetryMediaFetch` (the canonical in-loop
transient policy) plus shutdown/abort `fetch_failed`. A restart-window abort is
the primary inbound-media loss vector and is recoverable on replay, whereas
in-loop retry mid-shutdown is futile; permanent `fetch_failed`, other 4xx, and
size limits stay non-retryable so they cannot loop in the spool.
Telegram reuses the shared helper instead of a local classifier
(`isAbortError`/`isTransientNetworkError` live in core; the classification must
not drift). Adds permanent-`fetch_failed` regression coverage at the unit and
behavioral level. Media-group partial delivery (#55216) unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(imessage): shed emoji anywhere in poll-vote echo match
iMessage native poll options carry a trailing emoji ("Lobster 🦞 ") while the
agent echoes its vote with a leading one ("🦞 Lobster.") under message_tool_only
reply mode. normalizePollEchoText stripped only a leading emoji prefix, so
"lobster 🦞" never matched "lobster" and the redundant text leaked past the
poll_vote_echo guard (shipped in #98421).
Shed emoji anywhere across every class: pictographic, regional-indicator flags
(🇺🇸), subdivision-flag tag chars, ZWJ, skin-tone, and keycap sequences cleared
as a unit (so "1️⃣" -> "" not "1", while a plain "1"/"#"/"*" survives). Internal
punctuation stays so C#/C++/Node.js remain distinct; emoji-only labels normalize
to empty and the guard's existing empty-option check fails open. Re-exports
normalizePollEchoText with a focused unit test across every emoji class plus a
message-tool integration test for the emoji-suffixed-option case.
* fix(imessage): make poll-echo emoji regex lint-safe
ClawSweeper/CI check-lint flagged no-misleading-character-class: combining
marks (VS16, U+20E3), ZWJ, and skin-tone modifiers are misleading inside a
single character class. Rewrite the emoji strip as an alternation using
property escapes (\p{Regional_Indicator}, \p{Emoji_Modifier}) plus standalone
atoms so the same normalization behavior passes lint. Behavior unchanged;
poll-echo unit + integration tests still green.
* fix(imessage): preserve poll option emoji identity
---------
Co-authored-by: Omar Shahine <10343873+omarshahine@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Adds an `on-exit` cron schedule kind: a job fires once when a watched command/process
exits, via gateway ProcessSupervisor exit watchers. Covers CLI (`--on-exit`/`--on-exit-cwd`),
tool/protocol schema, RPC list-filter, Control UI + macOS read-only display, SQLite
round-trip, and origin-aware wake routing. Restart-safe one-shot (persists completion
before firing); platform-aware shell; bounded watched-command execution.
Squashed from 22 iterative commits for a clean rebase onto current main.
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).
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>
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