* fix(ports): ignore malformed lsof listener pids
* fix(infra): strictly reject malformed lsof pid tokens
Number.parseInt truncated tokens like "111abc" to 111, silently accepting
a corrupted lsof pid record. Switch to parseStrictPositiveInteger so the
whole token must be a valid positive integer, and restore the deduped
listener test that a prior change had replaced instead of extended.
* fix(cli): strictly reject malformed lsof pid tokens
Harden the force-free lsof parser to reject digit-prefixed garbage like
p111abc instead of truncating it to pid 111, and cover the sibling parser
with a focused regression test.
* fix(infra): strictly parse stale lsof pid tokens
Harden the stale gateway cleanup parser to reject digit-prefixed garbage
like p111abc instead of truncating it to pid 111, and add a focused
regression test without dropping the existing argv verification coverage.
* fix(ports): fail closed on malformed lsof pids
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(media): allow aws-sdk auth mode for image and audio/video paths
Media understanding tools failed for amazon-bedrock deployments using
auth.mode "aws-sdk" (BYOK via role/SSO/profile creds). Each path called
requireApiKey, which throws on the empty-key sentinel before the AWS SDK
credential chain can resolve creds at call time.
The image path (image.ts) resolves auth via getApiKeyForModel; the
audio/video paths route through resolveProviderExecutionAuth. Both now
mirror the chat path's allowMissingApiKeyModes allowance: when the
resolved key is empty and the mode is aws-sdk, execute keyless and let
the SDK resolve credentials. The image path skips setRuntimeApiKey so no
empty-string secret is persisted; audio/video return the kind:"none"
execution auth so the runner bypasses key rotation.
Closes#72031
* fix(media): cover Bedrock PDF SDK auth
* fix(pdf): register plugin completion streams
* fix(media): recognize Bedrock SDK tool auth
* chore: move release note to PR
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
resolveSupportedThinkingLevelFromProfile sorts the supported levels descending,
then, when every supported level exceeds the request (e.g. a stored `off` on a
profile that has no off level), fell through to `ranked.find(non-off)` — the
first entry of a descending list, i.e. the most expensive level. A session
carrying thinking `off` that switched onto such a model was silently remapped to
maximum reasoning and persisted (src/auto-reply/reply/directive-handling.persist.ts).
Use `ranked.findLast(non-off)` so a below-range request resolves to the cheapest
non-off level instead. Regression test included (red before, green after).
This surfaces with the first bundled no-off thinking profiles (Fireworks
GPT-OSS 120B, see #92217), but the clamp lives in shared core logic, so it ships
on its own.
* fix(gateway): show last error when status probe fails
* fix(gateway): scope status log diagnostics
---------
Co-authored-by: wAngByg <wAngByg@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(tui): suppress adjacent history-replay/live-final duplicate in ChatLog (#96967)
When sessions.changed triggers loadHistory(), assistant rows are replayed
via finalizeAssistant(text) without a runId. A late live final with the
same text and a runId can land immediately afterward, producing a visible
duplicate assistant reply.
Track the most recent history-replay assistant component in ChatLog and
suppress a following live final when the text matches and no other
component was appended in between. This is simpler and more targeted
than the prior event-handler approach because it operates at the
rendering layer where the duplicate is actually visible.
Fixes#96967
* fix(tui): bound replay-final dedupe window in ChatLog
* fix(tui): bound replay-final dedupe marker with TTL expiration
* fix(tui): satisfy no-promise-executor-return lint rule in chat-log test
* fix(tui): replace ChatLog text heuristic with runId-based surrender tracking
Drop the lastHistoryAssistant text-matching dedupe in ChatLog
(posed P1 false-suppression risk). Instead, track active runIds
surrendered to loadHistory() on sessions.changed 'new' — late
events for surrendered runs are suppressed by exact runId match.
Add ChatLog.hasStreamingRun() so the event handler can detect
when loadHistory restored a surrendered run as in-flight streaming.
Retain chatFinalizedRuns / chatFinalizedRunsWithDisplay from the
original fix for post-reload delta/error dedup of finalized runs.
Incorporates the precise surrender-tracking approach from closed
PRs #96979/#96986 while keeping the scoped ChatLog API minimal.
* fix(tui): render displayable late final for surrendered non-streaming run
When a sessions.changed 'new' surrenders an in-flight run and
loadHistory does not restore it as streaming, a late displayable
final must still be rendered — otherwise the user never sees the
assistant reply. Only suppress non-displayable finals (empty
message, no errorMessage).
Add focused test for non-displayable final suppression.
* test(tui): add coverage for empty in-flight restore + late displayable final
Covers the rank-up scenario from #96979: surrendered run restored
by loadHistory as empty streaming, late displayable final passes
through surrender check and renders to chatLog.
* fix(tui): suppress late finals for surrendered finalized runs to prevent history-replay duplicate
When sessions.changed 'new' fires, now surrender chatFinalizedRuns entries
too (as 'finalized'), not just sessionRuns ('in-flight'). This prevents
the finalized-before-reload history-replay duplicate: a late final for an
already-displayed run gets suppressed because the history-replay static
row covers the visible content.
In-flight surrendered runs still render displayable finals because the
user may not have seen the streaming text before the reload.
SurrenderedToHistoryRunIds is now Map<string,'in-flight'|'finalized'>
so the surrender check can branch on source. Move surrender check before
the finalizedRuns/chatFinalizedRuns guard so surrendered finalized runs
are routed correctly.
Add focused tests for history-replayed-visible (suppress) and
history-replay-missing (render) late finals.
* fix(tui): extend surrender coverage to sessions.changed reset reload path
Reset also calls clearTrackedRunState() + loadHistory() with the same
sessionKey, so late chat events for runs from the old session window
can race with the history replay the same way as 'new'. Surrender
sessionRuns and chatFinalizedRuns on both reasons (#96979 P1 rank-up).
* fix(tui): keep surrendered finalized marker after late delta to block later final
For finalized surrendered runs, a late delta must not clear the
surrender marker — otherwise a subsequent late final would slip
through and produce a duplicate. Only in-flight surrendered runs
clear the marker on delta (their final needs to render).
* fix(tui): gate session history reloads on persistence
Co-authored-by: xialonglee <li.xialong@xydigit.com>
* docs(changelog): note TUI external-run dedupe
* test(tui): type history load harness results
* test(tui): defer history results without async mocks
* chore(changelog): defer TUI note to release
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(update): keep self-updates on the running install's global root
Package self-updates resolved their install target from `npm root -g`
even when the running install's package root was already known. In an
environment where PATH's npm does not belong to the running install's
Node tree (a launchd/systemd service PATH, or mixed Homebrew + nvm
setups), the probe points at a different global root — or derives a
brand-new one — so the update installs there, reports success, and
leaves the running install stale.
Observed in the wild: a gateway-managed `openclaw update --yes` on
2026.5.28 ran nvm's npm under Homebrew's node, npm derived its default
prefix from the realpathed execPath, and the update installed a full
copy into /opt/homebrew/Cellar/node/26.3.1/lib/node_modules/openclaw
("before": null — a tree that had never existed) while the running nvm
install stayed on the old version until a manual shell update.
resolveGlobalInstallTarget now prefers the package root's own global
root for npm targets; the `npm root -g` probe only decides the target
when no package root is known, and an ephemeral per-Node probe result
(Homebrew Cellar, nvm/n/asdf/volta version dirs — the same set
#93650 filters npm commands against) is never adopted.
* fix(update): infer global roots for scoped package roots
The running-root fallback missed scoped installs because
inferGlobalRootFromPackageRoot only accepted an immediate node_modules
parent. Hop over an @scope segment so node_modules/@scope/name resolves
to the same global root. Regression test: scoped package root with an
ephemeral probe now falls back to the running root.
* fix(update): scope running-root selection
* docs(changelog): note npm self-update root fix
* chore(changelog): defer release note
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The reload handler no-op config test enables fake timers and drains all timers. In cross-file non-isolated runs, server-startup-post-attach closes the shared state DB first, so this test reopens it through the managed config reloader's default installed-plugin record read.
That DB open schedules SQLite WAL maintenance at src/infra/sqlite-wal.ts:345 while fake timers are active, and vi.runAllTimersAsync spins the interval until Vitest aborts after 10000 timers. Mock installed plugin record reads in this handler test so the test owns only the reload debounce timer it is trying to drain.
* fix(macos): preserve device identity storage
Avoid selecting App Group identity storage when the running app lacks the matching application-groups entitlement, so unentitled macOS builds fall back to the legacy writable identity directory. Existing unrecognized identity files are now preserved instead of being replaced by a newly generated identity, matching the no-overwrite behavior already used for recognized invalid identity data.
* fix(macos): repair identity CI coverage
Limit the App Group entitlement probe to macOS builds so iOS keeps its existing App Group path behavior. Update the state-dir identity regression to assert the new no-overwrite contract for invalid identity-shaped files.
* fix(macos): migrate existing app group identity on legacy fallback
Unentitled macOS builds now migrate a readable App Group device identity
into the selected legacy store before falling back, so an upgrade preserves
the existing device id instead of minting a new one.
* fix(macos): migrate device auth store with identity on app group fallback
The app group to legacy identity migration now carries the sibling
device-auth store file, so an unentitled build keeps its stored device
tokens (keyed by the migrated deviceId) instead of re-pairing. The
process-immutable entitlement check is resolved once per process rather
than creating a SecTask on every state-dir lookup, and the migration
source API returns an identity+auth pair struct.
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The shared provider OAuth success/error page (OpenAI/Codex and Anthropic
login flows) still embedded the legacy Pi logo SVG. Swap it for the
OpenClaw lobster mascot, matching ui/public/favicon.svg.
The active user turn was decorated at request-build time with an inbound
metadata block (Conversation info / reply-target / sender / forwarded /
chat-history), but the session store persists the bare text and the LLM boundary
strips that block from historical replay. So a user message's serialized bytes
changed retroactively — decorated as the active turn, bare as history — on every
turn, invalidating any prefix/exact-match provider prompt cache from that point
onward, every turn. (#90811 fixed the timestamp half of this same asymmetry.)
Rework of the earlier "strip the active turn" approach (which lost model-visible
context — ClawSweeper's P1) into one that keeps the context AND recovers the
cache cost:
- Route current-turn inbound metadata out of the user prompt and into the hidden
runtime-context custom message (the mechanism from openclaw#89428), and
relocate that carrier to the ABSOLUTE TAIL of the wire request (after the
active user turn and any tool-call scaffolding). The user turn stays bare and
byte-identical in both the active and historical positions; the volatile
carrier vanishes next turn exactly where the assistant reply begins anyway, so
the request is an append-only prefix-extension through the active user turn.
- A durable marker (UserMessage.runtimeContextCarrier, set by convertToLlm from
the carrier's details) lets the Anthropic SDK transport, the managed Anthropic
transport (anthropic-transport-stream / anthropic-payload-policy), and
OpenAI-completions skip the carrier when selecting the deepest cache_control
breakpoint, keeping the anchor on the last stable user turn.
Storage stays BARE (invariant preserved); runtime-only turns (room events) keep
their existing inline behavior, which is byte-stable because room-event/system
context is not strip-eligible; legacy bare-stored sessions are unchanged.
Reviewed with adversarial gpt-5.5/codex sweeps (correctness, byte-identity,
compatibility, test adequacy): found + fixed the managed Anthropic transport
cache-anchor path and added the missing OpenAI-completions/managed-transport
breakpoint tests; a theoretical runtime-only mutation was verified unreachable
and pinned with a test. Final sweep clean.
Co-authored-by: openclaw#89428 (runtime-context handoff approach)
buildOpenAIClientHeaders only skipped generating a session_id header
when the exact lowercase key was already present, so a caller-supplied
Session_ID or SESSION_ID header would end up on the wire alongside our
generated one instead of winning per the caller-wins header contract.
Check all resolved header keys case-insensitively before injecting.
The ChatGPT backend routes requests by session_id (codex-cli sends it); without it each request lands on an arbitrary machine and the prompt cache misses. Measured on live traffic: 56.0% -> 81.8% TRUE cache hit rate (12-turn A/B, identical prompts).
* 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>
Behavior: a plugin before_agent_finalize hook that never resolves could previously freeze an agent run forever after a successful compaction retry, with no errors and no recovery from a gateway restart; this was the frozen-runner mechanism behind #84777. before_agent_finalize now has the same 15s default budget as sibling modifying hooks and fails open with the original final answer, converting the freeze into a bounded delay.
Surface: plugin hook runner defaults (src/plugins/hooks.ts), docs/plugins/hooks.md.
Refs #84777.
Fixes rough edges in the standalone install flow (install.sh -> openclaw onboard), found and verified by running the flow in a clean container and on a clean macOS Tahoe VM:
- Provider auth setup failures (e.g. the preselected "Anthropic Claude CLI" option on a host without a Claude CLI login) no longer kill the whole wizard. The interactive wizard notes the error and returns to the provider picker; explicit --auth-choice automation still fails fast.
- Onboarding config now persists before the channel/search/skills steps, so a crash or cancel during channel pairing no longer loses auth + gateway decisions.
- With model auth skipped, finalize no longer auto-sends the "Wake up, my friend!" message (which always failed with a provider auth error). The hatch seed is gated on usable model credentials and a "Model auth missing" note explains the next step.
- Search provider picker no longer labels non-key credentials (e.g. SearXNG base URL) as "API key required".
- install.sh no longer warns "PATH missing npm global bin dir" with manual fix steps after it already persisted the export line; it reports the PATH was updated and how to reload the current shell.
- Removed the dead interactive hooks onboarding step (setupInternalHooks); quickstart enables default hooks silently.
Verified live per fix in a clean Debian/Node 24 container and on a clean macOS 26.5 Parallels VM (wizard re-prompt, SearXNG label), plus wizard/onboard test suites and tsgo:core.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
* fix(acp): record cancelled background turns as cancelled, not succeeded
A cancelled parented ACP turn resolves without throwing (only failed turns
throw), so the turn-runner success path recorded the detached task as
succeeded. Thread the terminal status out of consumeAcpTurnStream and branch
at the runner; widen markBackgroundTaskTerminal to accept cancelled, routed
through the existing failTaskRunByRunId sink. The runTurn fallback infers
cancellation from done.stopReason, mirroring the acpx adapter.
* fix(acp): narrow cancelled task status handling
Co-authored-by: masatohoshino <g515hoshino@gmail.com>
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Tracks per-skill usage from the skill.used diagnostic event (trusted-only
delivery, file-scoped identity), sweeps workshop-created skills daily from
gateway maintenance (active -> stale 30d -> archived 90d, pinned bypass,
restore-only unarchive, files never touched), filters archived skills from
snapshots fail-open, reports workspace-scoped overlap candidates, and adds
openclaw skills curator CLI, additive gateway methods, and a warn-only
doctor finding. Zero new config keys; SQLite/Kysely state only.
* fix(agent-model): omit synthesized maxTokens fallback (Fixes#98295)
The configured-fallback resolver used DEFAULT_CONTEXT_TOKENS (200_000) as
the last-resort value for model.maxTokens when no configured, provider,
or bundled-catalog value was known. For strict OpenAI-compatible providers,
model.maxTokens is forwarded as max_completion_tokens on every request,
so the synthesized 200k value became a wire-level output cap that exceeded
the provider ceiling and produced HTTP 400 (Param Incorrect) on providers
like Xiaomi MiMo (131_072 max) — reported for mimo-v2.5 / mimo-v2.5-pro
added via 'models add' on a custom provider entry.
The sibling applyConfiguredProviderOverrides path already omits maxTokens
when nothing resolves; align resolveConfiguredFallbackModel with that
behavior. When the transport sees an undefined model.maxTokens, it omits
max_completion_tokens entirely and the provider applies its own default,
which is the correct behavior when the user has configured no output limit.
contextWindow retains DEFAULT_CONTEXT_TOKENS as its local budgeting
fallback (it is not shipped on the wire).
* test(agents): prove unknown output cap omission
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>