A prepared OpenClaw auth handoff was still resolved for connections that
target the operator's native Codex home, so every turn failed with
"Prepared Codex auth requires an isolated app-server home." once a stored
OpenAI profile produced a prepared model route. User-home mode now keeps
its native account for both subscription and Platform routes, matching the
documented contract; the shared-client guard stays as the backstop.
* fix(agents): close symlink-then-.. workspace boundary bypass in assertSandboxPath
resolveSandboxPath builds on path.resolve, which collapses ".." lexically
before any symlink is resolved. When a "<symlink>/.." sequence appears, the
lexical collapse lands on a harmless in-root path while the OS resolves the
same raw string (symlink first, then "..") to a location outside the
workspace, so assertSandboxPath approved inputs whose real resolution escaped
the boundary.
Add assertRawParentWithinRoot: it resolves the raw (non-collapsed) parent
chain via the OS realpath (fs.realpath.native; the JS realpath and
path.resolve both pre-collapse "..") and asserts it stays inside the
workspace root. It runs after assertNoPathAliasEscape so that guard's more
specific messages still win for cases it already catches, adding coverage only
for the residual gap it never sees.
Not currently reachable through shipped tools (read/write/edit route I/O
through fs-safe Root, which opens its own collapsed path; other callers use
the returned .resolved) -- this hardens the validator so the boundary no
longer depends on caller discipline. Regression test included.
* fix(agents): simplify raw sandbox path guard
* fix(agents): preserve symlinked sandbox roots
* fix(agents): validate raw final path aliases
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(ui): back off unavailable session PR polling
The sidebar PR indicators iterated worktree rows sequentially; each ~68ms
UNAVAILABLE reply immediately advanced to the next row, hammering
controlUi.sessionPullRequests at ~14 calls/second forever (the method is
advertised while its runtime dependency is down, so the advertised-method
gate never stopped it).
Route all callers through GatewayBrowserClient.requestSessionPullRequests:
a non-retryable UNAVAILABLE latches until the next socket hello; retryable
failures back off 30s doubling to 5min, honoring longer server retry hints.
A generation counter keeps concurrent rows from multiplying one outage wave.
* fix(ui): satisfy test-types and knip lanes
Type the PR-indicator request mocks with real signatures instead of bare
vi.fn shells, and keep GatewaySessionPullRequestsParams module-local until a
second consumer exists.
* test(ui): route hydration stubs through the PR latch wrapper
* chore(ui): drop unused ControlUiSessionPullRequests re-export
* test(ui): stub the PR latch wrapper in sidebar and clobber fixtures
* fix(ui): keep stable chat rows in insertion order and only sort live tool/stream items by timestamp
* fix(ui): keep live rows within current turn
* fix(ui): keep current work above queued turns
* fix(ui): keep streamed replies above queued turns
* fix(ui): preserve reconnecting run order
* fix(ui): preserve causal terminal ordering
* fix(ui): bound replay rows to owning turns
* fix(ui): keep question summaries in owning turns
* fix(ui): scope question run ownership to session
* fix(ui): restore reconnecting chat run identity
* fix(ui): remove unused chatItemTimestamp import in chat-thread-build.ts
* fix(ui): correlate question summaries with agent runs
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
* chore(i18n): refresh native source baseline
Co-authored-by: Peter Lee <li.xialong@xydigit.com>
* fix(ui): remove unused chatItemTimestamp export and split tool-stream test file
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(sessions): add SQLite session entry cache with current-schema support
Rebase PR #113959 cache changes onto current origin/main:
- Replace listSqliteSessionEntries with cached version using
session_nodes table (was session_entries)
- Add connection-identity token for data_version cross-connection safety
- Add light listing variant with LIMIT/OFFSET pagination
- Add file-stat + data_version coherence guards
- Add cache invalidation on all mutation paths
- Add SessionEntryListScope type with light/limit/offset fields
- Restore store-cache.ts (deleted upstream, re-added with cache layer)
* perf(sessions): project lightweight gateway lists
Replace the request-time session cache and freshness probes with an explicit SQL-side list projection. Keep full entry reads as the default, preserve the post-await sharing authorization refresh, and carry the projection through Gateway, TUI, and embedded-tool list paths.
Co-authored-by: Erick Kinnee <1707617+ekinnee@users.noreply.github.com>
Co-authored-by: gucasbrg <buruguo2000@163.com>
* test(tui): expect lightweight session projection
---------
Co-authored-by: Erick Kinnee <ekinnee@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: gucasbrg <buruguo2000@163.com>
* fix(mcp): reconnect a bundled MCP server instead of wedging on it
A bundled MCP server that dies or stops responding never comes back. Two gaps:
1. `client.onclose` flips `connected`/`disconnectReason` and stops there. The
dead session stays in `sessions` and the memoized `catalog` stays valid, so
every later request is a `getCatalog()` cache hit followed by
`requireConnectedSession` throwing `is disconnected` — indefinitely. The
reconnect path already exists (the catalog builder retires a disconnected
session and rebuilds it); it just never runs, because nothing invalidates the
catalog on close. This mirrors what `listChanged.onChanged` already does.
2. At `BUNDLE_MCP_FAILURE_THRESHOLD`, `recordServerToolFailure` sets
`retryAfterMs`. That delays the next attempt against the same broken
transport rather than repairing it, so a server that hangs stays hung.
Recovery therefore depended entirely on idle eviction
(`mcp.sessionIdleTtlMs`), a `configFingerprint` change, or a restart — which
makes the eviction timer a de facto health mechanism and couples "how fast a
broken server recovers" to "how long an idle one is kept". Raising the TTL for
warm-start latency silently makes outages last longer.
Both fixes are narrowed to avoid over-reacting:
- onclose only invalidates for a connection that was ESTABLISHED and lost. A
transport closing mid-connect is the startup-failure path the catalog builder
already owns; invalidating there discards the in-flight build on a generation
mismatch and spins up a respawn loop.
- recycling requires the server to have stopped RESPONDING
(`ErrorCode.RequestTimeout`, or a session no longer connected). A server
answering with JSON-RPC errors is healthy and reachable — tearing down its
transport because a tool got bad arguments would be worse than the bug. The
existing cooldown still covers that case, as its tests require.
`serverBackoff.delete` on recycle is deliberate: the replacement must not
inherit the dead transport's failure count.
Two tests encoded the old behavior and now assert recovery instead, including
that the replacement child has a different pid. The recovery assertion polls: a
request already in flight when the transport closes still fails, and should.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(mcp): recover disconnected bundled servers
Co-authored-by: Vlad Gaevsky <kelstar95@gmail.com>
* test(mcp): tighten recovery validation
Co-authored-by: Vlad Gaevsky <kelstar95@gmail.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(cron): bind agent turns to current sessions
* feat(commands): add loop chat command
* test(cron): cover session defaults and loop commands
* docs(cron): document current defaults and loops
* fix(commands): scope /loop status and stop by conversation name tag
* fix(commands): widen /loop conversation tag to 48 bits
* fix(commands): include disabled jobs in /loop status and stop
* docs(cron): regenerate docs map
* test(cron): split session-target default tests to satisfy max-lines
* fix(plugins): stop treating a partial active registry as authoritative for web providers
An active plugin registry with some web providers used to win even when a
manifest-declared candidate (e.g. an npm-installed Brave plugin with
BRAVE_API_KEY set) was absent from it, so env-var auto-detect could never see
installed external search providers. Delegate to the coverage-checked
resolvePluginWebProviders path, which reuses the active registry only when it
covers every declared candidate.
* feat(agents): require explicit overwrite before replacing pre-existing files in the write tool
Blind writes to an existing path silently destroyed user content (observed as
WildClawBench safety-task data loss with weaker models). The write tool now
refuses to replace an existing differing file unless the call passes
overwrite:true or this tool instance already wrote that path, keeping
iterate-loops friction-free while making destructive replacement an explicit
model decision.
* docs(templates): make a concrete first-message task outrank the BOOTSTRAP.md birth sequence
A fresh workspace's birth ritual hijacked substantive first messages: agents
introduced themselves and asked for a name instead of doing the requested
work (worst with weaker models, which follow the ritual literally). State
task precedence explicitly at the top of the template.
* fix(agents): lead the write overwrite guard error with the safe protocol
Weak models retried immediately with overwrite:true when the flag came first
in the message. Order the guidance read -> rename -> overwrite-as-last-resort
so the destructive path requires an explicit judgment call.
* refactor(agents): replace the write overwrite flag with a confirm-by-resend gate
The overwrite:true escape hatch let weak models bulldoze reflexively and grew
the tool schema. The first write to a differing pre-existing file now returns
that file's content (head-clipped) and a resend of the identical write, issued
after the warning, against byte-identical existing content confirms the
replacement. Fingerprints hash raw bytes; oversized files fall back to
size+mtime (named tradeoff); missing metadata fails closed.
* revert(agents): restore plain overwrite semantics in the write tool
The overwrite gate (flag, then confirm-by-resend) was overfit to one
WildClawBench safety rubric: the guarded model still chose to overwrite and
still scored zero, while every real overwrite in normal sessions paid a
round-trip. Write means write; peers (Pi, Hermes) agree.
* test(agents): drop the stale overwrite arg from the write output-contract test