* refactor(sessions): migrate runtime storage to sqlite
* test(sessions): fix sqlite CI regressions
* test(sessions): align remaining sqlite fixtures
* fix(codex): require sqlite trajectory recorder
* test(sessions): align orphan recovery sqlite fixture
* test(sessions): align sqlite rebase fixtures
* fix(sessions): finish current-main integration of the sqlite flip
Resolve the whole-store SDK removal across its owner boundary: drop the
loadSessionStore re-export and the registry whole-store wrappers, wire
hasTrackedActiveSessionRun into gateway chat, complete the
preserveLockedHarnessIds cleanup contract, flip the codex thread-history
import to storePath targets, and port remaining main-side tests from
file-store helpers to session accessor reads.
* chore: drop committed pebbles log, revert plugin-inspector bump, refresh generated docs
Remove the 1.8k-line .pebbles/events.jsonl work log from the branch, restore
the plugin-inspector advisory lane to main's pinned 0.3.10 so the supply-chain
bump gets its own review, and regenerate docs_map, the plugin SDK API baseline,
and the export-surface ratchet for the merged tree.
* feat(sessions): keep archived transcripts by default with zstd cold storage
Codex-style retention: deleting or resetting a session archives its
transcript as a zstd-compressed JSONL artifact (plain when the runtime
lacks node:zlib zstd) and keeps it until the disk budget evicts oldest
first. resetArchiveRetention now governs both deleted and reset archives
and defaults to keep; maxDiskBytes defaults to 2gb so retention stays
bounded, with archives evicted before live sessions. The cron reaper
follows the same knob instead of deleting archives on its own timer.
* fix(state): converge agent DB migration lineages and bound database growth
Merge coherence: run both structure-gated legacy memory-schema repairs
(flip-lineage drop, main-lineage identity rebuild) before the flip
migration so pre-flip v1/v2 and pre-merge flip v1/v4 databases all
converge, and hoist foreign_keys=OFF outside the schema transaction
where the pragma was silently ignored and the v1 sessions rebuild
cascade-deleted session_entries.
Growth guards: fresh agent DBs enable auto_vacuum=INCREMENTAL, WAL
maintenance releases freed pages in bounded passes (never a blocking
full VACUUM), and doctor reports state/agent DB bloat from freelist
stats.
* fix(codex): resolve the store path for thread-history import via the SDK
The supervision catalog passed the legacy sessionFile locator to the
storePath-targeted transcript mirror; resolve the agent store path with
the session-store SDK helper instead of a runtime-object seam so test
fakes and headless callers need no extra surface. Drop the obsolete
missing-session-id preprocessing case: sessions rows are NOT NULL on
session_id and upsert repairs id-less patches at write time.
* fix(sessions): fail safe on malformed disk-budget config and doctor stat errors
A malformed explicit maxDiskBytes disables the budget instead of
falling back to the destructive 2gb default the user never chose, and
the doctor bloat check skips databases whose paths stat-fail instead of
aborting doctor.
* fix(sessions): complete sqlite conflict translations
* test(sqlite): align hardening checks with maintenance
* test(sessions): inspect compressed transcript archives
* fix(tests): await session seeds and drop unused helpers flagged by CI lint
The five unawaited writeSessionStoreSeed calls raced their SQLite seeds
against the assertions, failing compact shards; the bloat probe drops a
useless initializer and the merged tests drop now-unused helpers.
* test(sessions): type legacy proof events directly
* test(sessions): align hardening contracts
* perf(sessions): read usage transcript sizes from SQL aggregates
Usage/cost scans walked every session and materialized every transcript
event just to re-stringify it for a byte estimate — the #86718 stall
class reborn on the DB. readTranscriptStatsSync sums stored JSON bytes
in SQLite without loading a single row.
* fix(sessions): re-root foreign-root transcript paths onto the current sessions dir
Restored backups, moved OPENCLAW_STATE_DIR, and rehearsal copies carry
absolute sessionFile paths from the old root; the containment fallback
kept those foreign paths, so migration read (and would archive) files in
the original root and reported local copies missing. Re-root the
canonical agents/<id>/sessions suffix onto the current dir when the file
exists there; genuine cross-root layouts still fall through unchanged.
* test(agents): seed harness admission through sqlite
* fix(sqlite): close agent db on pragma setup failure
* fix(doctor): compact and retrofit incremental auto-vacuum after session import
The migration is the sanctioned offline window: post-import compact
reclaims import churn and applies auto_vacuum=INCREMENTAL to databases
created before the fresh-DB pragma existed, so runtime maintenance can
release pages in bounded passes on every install.
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(write): report actual UTF-8 byte count in success message
Replace string.length (UTF-16 code units) with Buffer.byteLength(content, 'utf8')
so the reported byte count matches the actual bytes written to disk.
For ASCII content the values are identical. For non-ASCII content (CJK, emoji),
string.length underreports — e.g. '你好' reports as 2 bytes but is actually 6.
* refactor(write): centralize success reporting
* test(write): avoid shadowing recovery fixture
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat: add metadata-only message audit events
* chore(protocol): restore generated swift models and config baseline after rebase
* fix(state): gate agent-db ownership check before schema version
* fix(cli): sync audit command description into the root-help catalog
* fix(audit): require destination proof before classifying outbound messages as direct
* refactor(audit): drop dead migration guard and add contract comments
* docs(gateway): add dedicated audit history page and cross-links
* test(e2e): enable direct-mode message audit in the telegram proof SUT config
* test(channels): expect declared conversationKind in durable delivery session context
* fix(audit): record the routing channel id for inbound rows from plugin channels
* fix(audit): match channel-prefixed delivery targets in the destination route gate
* fix(audit): validate explicit target kind against the destination route kind
* fix(audit): use the canonical target-prefix grammar in the destination route gate and fail closed on foreign migration tables
* fix(audit): normalize nested provider and kind target prefixes in the destination gate
* fix(audit): strip registered provider aliases and the direct kind prefix in the destination gate
* chore(docs): refresh config baseline after rebase
* fix(agents): protect provider auth exchange output by sensitivity, not input marker
protectPreparedProviderRuntimeAuth decided whether to protect a credential from
the shape of the input sourceApiKey: if the input was not a sentinel and not
registered for redaction it returned the exchange output untouched. That
assumes output sensitivity mirrors input sensitivity, which is false for any
provider that turns a non-secret marker into a real credential.
The bundled amazon-bedrock-mantle plugin hits this live: its stored apiKey is
the marker __amazon_bedrock_mantle_iam__ (not declared in nonSecretAuthMarkers,
so resolved raw), and its prepareRuntimeAuth mints a real IAM-derived Bedrock
bearer token. That real token flowed through the guard unprotected, so it was
stored and logged unredacted.
Gate protection per output value instead: skip empty values and non-secret
markers, protect everything else. Sibling providers that echo their own markers
(ollama-local, lmstudio-local, codex-app-server, gcp-vertex-credentials) are
unaffected. Drops the now-dead sourceApiKey parameter and its call sites.
* test(agents): assert sentinel storage and egress restoration for protected provider auth
* test(agents): assert protected runtime auth state
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(sessions): durable session state events with parent invalidation
Parents no longer act on stale child-session state: a durable, typed
signal log (session_state_events + per-agent heads) records direct human
messages to children, child spawn/terminal outcomes, goal changes, and
compaction at the seams where those facts are created. A frozen-watermark
cursor protocol (session_watch_cursors) delivers one coalesced system
notice to the parent through the existing system-event + heartbeat wake
idiom, acknowledged at the shared generic drain, with a deferred startup
sweep for restart recovery. session_status gains stateVersion and
changesSince (with exact pruned-history gap signaling); sessions_list
rows gain stateVersion.
Closes#104565
* chore: regenerate docs map and prompt snapshots for session_status changesSince
* fix(outbound): preserve backticks on <code> tags with attributes
The plain-text sanitizer only matched bare <code> openers, so attributed
variants such as <code class="language-ts"> lost their backtick wrapping
and were stripped to raw text before channel delivery.
Allow optional attributes on the opening <code> tag, consistent with the
existing handling for <h[1-6]> and <li> in the same function.
Fixes#104117
* fix(outbound): preserve attributed inline formatting
Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
* test(outbound): compact attributed tag coverage
* fix(outbound): normalize attributed formatting tags
* docs(outbound): clarify attribute normalization invariant
* fix(outbound): preserve native formatting semantics
* docs(plugin-sdk): document sanitizer markup styles
* docs(plugin-sdk): refresh docs map
---------
Co-authored-by: moguangyu5-design <moguangyu5-design@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com>
* feat(fleet): add openclaw fleet cell supervisor for multi-tenant hosting
* test(fleet): cover cell profile, registry, containers, and service flows
* docs(fleet): document multi-tenant hosting and the fleet CLI
* fix(fleet): verify upgrade replacements and release foreign-collision reservations
* fix(fleet): gate upgrade commit on replacement health
* docs(fleet): mark fleet experimental and pin its single-host scope
* test(fleet): make incomplete-profile casts explicit for test-type lane
* fix(state): regenerate kysely schema artifacts after rebase conflict
* fix(gateway): allow macOS app platform version refresh without re-pairing
When macOS updates (e.g. 26.5.0 → 26.5.1), the Mac app detects a
platform mismatch against its paired record and sends a repair request.
The approval dialog enters an infinite loop because the pending request
TTL (5 min) expires before the user clicks Approve, causing "unknown
requestId" errors that trigger a new repair request ad infinitum.
iOS and Android already had silent platform version refresh via
normalizeMobileAppPlatformPin — this extends the same treatment to the
macOS native app by recognizing "macos X.Y.Z" as the same platform
family regardless of version, renaming the helper to
normalizeNativeAppPlatformPin.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(gateway): validate macOS platform version refreshes
* fix(gateway): avoid native platform family marker collisions
* test(gateway): type node client name
---------
Co-authored-by: 骆毅 <luoyifu.lyf@digital-engine.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat: node-hosted plugins — dynamic tools, MCP servers, and skills
Nodes become declarative plugin hosts:
- node.pluginTools.update: node hosts publish plugin-registered agent tool
descriptors; gateway materializes them as agent tools executing via
node.invoke under the node command allowlist, with tools.effective
invalidation and node online/offline removal.
- Trusted paired-node descriptors: no gateway-side plugin registration
required; gateway.nodes.pluginTools.enabled off-switch (default on);
description/count caps; deterministic node-prefixed collision names.
- Declarative node-hosted MCP: nodeHost.mcp.servers (McpServerConfig shape)
starts MCP clients on the node host, publishes tools as pluginId node-mcp,
executes via built-in mcp.tools.call.v1 with per-layer timeouts, failure
isolation, and orphan-safe shutdown. No re-pairing when servers change.
- Node-hosted skills: node.skills.update publishes ~/.openclaw/skills
content (64 skills/64KB/512KB caps both sides); gateway merges them into
the skills snapshot while connected and exec host=node is available, with
node:// locators, node-prefixed collisions, disabled command dispatch,
and gateway.nodes.skills.enabled + nodeHost.skills.enabled switches.
- Security: node-supplied pluginIds cannot satisfy pluginId-scoped tool
allowlists unless gateway-registered; reserved node-mcp id requires the
core MCP descriptor shape; protocol registry kept out of public
plugin-sdk dts.
- E2E: pond harness proves publication, MCP round-trip, skills locator, and
disconnect/reconnect for all three surfaces.
* style: format node-plugin-tools test
* fix(skills): keep status loader unfiltered when eligibility is passed
skills.status started passing eligibility for the node-skill merge, which
flipped loadWorkspaceSkillEntries into filtered mode and dropped disabled
skills from status reports (QA plugin-lifecycle-hot-reload timeout). Status
now merges node skills explicitly around an unfiltered load. Also: regen
docs_map for new node docs sections; add the intentional node-host MCP
onclose suppression to the lint-suppression allowlist.
Approving a node whose pairing surface advertises browser.proxy only required
operator.write, while invoking browser.proxy already requires operator.admin
(server-methods/nodes.ts). resolveNodePairApprovalScopes bumped the approval
scope to operator.admin only for NODE_SYSTEM_RUN_COMMANDS, so a write-scoped
operator could trust a browser.proxy-capable node that later routes bundled
browser-tool traffic. Add NODE_BROWSER_PROXY_COMMAND to the admin-approval set
so approval scope matches the invoke-time gate.
One RUN_STALE_TAKEOVER_MS constant and one resolveRunStaleThresholdMs in
diagnostic-run-activity.ts replace the hand-paired constants and duplicated
blocked-tool-floor derivations in the reply and embedded run registries.
Thresholds are byte-identical before and after; zero behavior change.
Part of #104219 (seam 4).