* feat(anthropic): gate live model discovery on contract coverage
Live catalog discovery cloned a template for any newly discovered Claude id, so
a future model generation would be selectable while request shaping treated it
as pre-4.6 and 400d. Add an opt-in acceptUnknownModel gate to the shared live
catalog seam and have the Anthropic plugin accept a discovered model only when
Anthropic's advertised capabilities agree with the contracts we would apply.
Manifest-published ids bypass the gate and keep their metadata.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(plugins): document the live-discovery admission hook
Record acceptUnknownModel in the provider-plugin SDK reference: when to use
it, that manifest-published ids bypass it, and the fail-closed guidance.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* refactor(agents): validate attestation hashes structurally
* fix(agents): reject Windows reserved device names in attestation filenames
* refactor(agents): close attestation filename set with an ASCII markdown charset
The attachment parser accumulated two parallel message strings via
conditional appends inside the offload loop (all markers vs non-image
markers). Markers are presentation-only now that MediaFact[] carries
identity, so the parser builds one message and the routing decision in
chat-send-attachments projects the image-stripped variant from the
structured offloaded refs (stripImageMediaMarkers) exactly where
routeImageOffloadsAsMediaPaths chose a string before. Marker order,
trailing placement, and trim semantics are byte-identical; goldens
unchanged.
* fix(gmail-setup): skip python interpreters gcloud can't use
resolvePythonExecutablePath accepted the first python3/python found on
PATH without checking its version, so macOS' bundled Python 3.9 (earlier
on PATH than a Homebrew 3.10-3.14 install) was chosen as CLOUDSDK_PYTHON
and gcloud failed to load. Query each candidate's version and skip any
outside gcloud's supported 3.10-3.14 range so a compatible interpreter
later on PATH is selected instead.
Closes#112712
* test(gmail): cover gcloud Python upper bound
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(sqlite): migrate persisted media to canonical facts and stop legacy writes
PR 3 of the media legacy retirement program — the operator-approved
canonical cutover.
- openclaw doctor --fix owns one idempotent migration: active
transcript_events rows canonicalize to __openclaw.media (facts-first
gap-fill, bare legacy kinds to fact.kind, transcribed indexes and
workspace dirs onto per-fact fields) via the transcript replacement
owner; cold plain/.zst archives rewrite through temp-file + codec
readback + event/id verification + atomic replace; trajectory
runtime snapshots canonicalize IN PLACE (telemetry preserved, never
row deletion). Invalid JSON, genuinely ambiguous legacy-only sparse
alignment, or a changed source aborts that owner without partial work;
reruns are no-ops.
- Per-agent schema advances to v16 as a pure downgrade guard (main
independently took v15 for board/session-sharing tables; no
columns/tables/indexes change here, shared-state DB untouched). v15
databases repair canonical indexes before the version assertion so
repairable installations never strand.
- The user-turn builder stops writing top-level legacy Media* fields;
shouldPersistStructuredMediaEntries and the aligned projection mode
are deleted; the generic transcript append boundary canonicalizes
every message role so SDK/mirror writers cannot mint new legacy rows.
- Internal persisted-reader legacy fallbacks are removed; the public
SDK projection stays until retirement PR 4's window expires.
Hardening from three adversarial review rounds, each with fixture
regressions: in-place trajectory canonicalization instead of row
deletion; repair-before-assert on the v15 path; all-roles append
canonicalization; duplicate-preserving exact row rewrites; v0-v15
reopen guards; complete canonical facts bypass compact legacy
projections (PR-1 dual-write rows migrate cleanly); SQLite LIKE
underscore escaped so populated foreign databases are never claimed.
* fix(sqlite): align schema-support metadata and gates with the v16 cutover
package.json agent schema support advances to 16; verifier and board
parity fixtures run doctor migration before steady-state access (the
production guards were correct); two test-only exports removed; the
migration module registered in the doctor raw-SQLite allowlist.
The runtime retirement already shipped: busy deferral is automatic,
reasoning payloads stay internal, the HEARTBEAT_OK ack budget is fixed at
300 chars, the Heartbeats prompt section follows cadence, and tool-error
warnings are always on. Docs still advertised skipWhenBusy, ackMaxChars,
includeReasoning, includeSystemPromptSection, and suppressToolErrorWarnings
as live options; this aligns seven pages with the fixed policies and the
strict heartbeat field list, locks the claw-profile skipWhenBusy rejection
diagnostic with a dedicated test, renames includeReasoning-era test/comments,
and canonicalizes claw add-plan workspace path assertions (macOS realpath).
* fix(compaction): thread agent streamFn into safeguard summarizer
The compaction safeguard summarizer invoked the model without the agent's
egress streamFn, so it fell back to the core HTTP client (completeSimple),
which builds the request from model.baseUrl. That field is empty ("") at
the core layer for OpenRouter-provider models — the real base URL is only
injected inside the provider plugin's stream function. The empty base URL
made the OpenAI SDK throw APIConnectionError, surfaced as
"Summarization failed: Connection error.", so safeguard compaction
summaries silently failed (the safeguard then preserved the verbatim
tail, masking it). Normal turns and the full/non-safeguard compaction
path were unaffected because they already thread streamFn.
Thread the agent's streamFn (the same this.agent.streamFn the core
compaction path passes into compact()) through the
session_before_compact event into summarizeViaLLM -> summarizeInStages
-> summarizeWithFallback -> summarizeChunks -> generateSummary, filling
the existing streamFn slot on the underlying summarization completion.
Minimal optional-param plumbing; safeguard tail-preservation behavior is
unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(compaction): preserve safeguard model execution context
* test(compaction): use canonical stream type export
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(sessions): always protect agent primary main session from maintenance eviction
capEntryCount computes maxRemovableEntries = maxEntries - preservedCount. When
protected thread/channel entries reach maxEntries, the only removable entry is
the unprotected agent:<id>:main session, so routine maintenance deletes the
operator's live TUI/webchat session out from under an in-flight run (surfacing
as "session file changed while embedded prompt lock was released"). The same
cap pressure also breaks subagent spawns.
Treat agent:<id>:main as always protected in isProtectedSessionMaintenanceEntry,
alongside existing thread/channel/topic protection.
Closes#112637
* fix(sessions): cover global primary session maintenance
* fix(sessions): preserve admitted sqlite sessions
* fix(sessions): preserve logical admission scope
* fix(sessions): align stale maintenance preflight
* test(sessions): keep maintenance preflight private
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test: consolidate OpenClaw test state fixtures
* test(plugin-sdk): expose isolated test state
Promote the isolated OpenClaw test-state lifecycle through a narrow published Plugin SDK subpath so extension tests no longer import private core helpers. This intentional SDK surface addition is maintainer-approved.
* test: use SDK test-state seam in extensions
Route bundled extension suites through the focused repo-local Plugin SDK test-state entrypoint and remove the Codex projector harness exports made stale by fixture consolidation. Keep the seam out of production builds and published package artifacts while auditing its real consumers in the full-tree deadcode scan.
* test(plugins): map test-state in package boundaries
* test(telegram): consolidate bot test harness
* test(plugin-sdk): expose isolated test state
Promote the isolated OpenClaw test-state lifecycle through a narrow published Plugin SDK subpath so extension tests no longer import private core helpers. This intentional SDK surface addition is maintainer-approved.
* test(telegram): use public test-state seam
* test(plugin-sdk): keep test state local-only
Match the existing channel test-helper boundary: bundled extension tests can import the focused SDK source entrypoint, while ordinary builds and the published npm package exclude it. The earlier public classification existed only on this unmerged PR branch and was never a shipped contract.
* test(plugins): map test-state in package boundaries