The Signal and iMessage channel plugins inferred approval prompts by
regex-sniffing rendered message text (header matching like 'Exec approval
required', /approve command parsing), violating the transport-only channel
doctrine: approval actions must stay typed until channel encoding.
The typed envelope already existed (channelData.execApproval with
approvalId/approvalKind/allowedDecisions) and every payload-level delivery
path consumed it; the regex paths were redundant re-derivation at the raw
send seam plus restart recovery for in-memory iMessage poll state.
- Signal: delete send-level prompt sniffing entirely; all approval sends
already flow through typed structured-payload or native-handler paths.
- iMessage: sendMessageIMessage takes a typed approvalPrompt binding
(id/kind/decisions) from the native approval handler instead of an
approvalKind flag plus text re-parsing.
- iMessage poller: persist pending poll targets in the plugin keyed store
so restart recovery no longer regex-scans chat history; typed recent-chat
discovery for handle-only DM targets stays. Split poll-target ownership
into approval-reaction-poll-targets.ts (max-lines).
- Plugin SDK: remove extractApprovalReactionPromptBinding — beta-only
surface, never in a stable release, so no deprecation window applies;
AGENTS.md now records that rule.
Accepted tradeoff: approval prompts delivered by a pre-upgrade process are
not rediscovered from chat text after restart (<=24h transient state;
persisted reaction bindings and event-driven tapbacks still work).
resolveFacadeModuleLocation only cached successful locations, so every
lookup for a plugin that is not installed re-walked the bundled-plugins
dir and registry on every request — request-time freshness polling of
process-stable install topology, which the architecture doctrine forbids.
Cache null results too. Install/reload/doctor flows already clear this
cache through registerPluginMetadataProcessMemoLifecycleClear, so a
newly installed plugin becomes visible exactly when the lifecycle owner
says so — the retry-on-appear test now pins that contract instead of
accidental per-call refresh.
* refactor(ai): give transport streams an honest writer type
* test(ai): use canonical transport stream fixtures
* fix(ai): preserve partial-less stream deltas
* fix(agents): allow required-preflight native Codex compaction
Required reply-preflight compaction on a Codex app-server-backed session
returns the intentional `ok: true, compacted: false` "codex app-server owns
automatic compaction" no-op because the preflight caller never passes
`allowNonManualNativeRequest`. The reply/preflight path then misclassifies
that successful skip as a failure and throws, dropping the user's turn with
"Context is too large and auto-compaction could not recover this turn." The
equivalent CLI path was fixed by #88207; this is the second, unpatched caller.
Route required-preflight through the existing private
`compactAfterContextEngine` harness capability (which already passes
`allowNonManualNativeRequest: true`) by adding a typed
`nativeCompactionRequest: "required_preflight" | "after_context_engine"`
origin on `maybeCompactAgentHarnessSession` and the Codex compact bridge.
The non-manual skip guard is bypassed for preflight, so Codex actually
compacts the thread.
A binding change between the initial read and the native request is a
stale-binding race, not a benign skip. For `required_preflight` (and the
non-manual CLI path) it now surfaces as the canonical recoverable
`stale_thread_binding` failure so the queued harness falls back to the
context engine instead of treating an uncompacted `ok: true` result as a
completed turn. A genuine post-context-engine request may still skip,
because the context engine has already compacted. Required-preflight is also
the one scoped exception to the model-locked terminal rule: missing or stale
Codex thread bindings recover via the shared context-engine fallback while
the persisted harness lock stays intact; other locked failures remain
terminal.
Rebased onto main after #120740 restructured the guarded native compaction
block; the recoverable-binding semantics are reintroduced on the new
structure and scoped by `nativeCompactionRequest` so #120740's
post-context-engine skip behavior is preserved.
Closes#119971.
* test(evidence): commit inspectable required-preflight live proof scripts for #119971
Adds the two live codex app-server proof scripts (binding-race +
locked-preflight) so the redacted terminal traces in the PR body are
inspectable on the exact head. Both drive the real codex binary and real
maybeCompactCodexAppServerSession with nativeCompactionRequest:
"required_preflight"; neither runs in CI (no codex binary).
* fix(agents): scope locked-preflight compaction fallback to Codex
Restrict the required-preflight model-lock exception to the Codex harness
so missing/stale thread bindings in other locked native harnesses (e.g.
Copilot) stay terminal instead of escaping the persisted model-lock
boundary via context-engine fallback. Add a model-locked Copilot
required-preflight regression covering both missing and stale thread
bindings.
* fix(codex): require native preflight compaction
* chore(plugin-sdk): account for native compaction exports
* test(codex): use complete cron authority fixtures
* chore(lint): shrink compaction assertion baseline
* fix(lint): honor root boundary timeout
* fix(lint): extend package boundary timeout
* fix(plugins): verify native compaction owner
---------
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
* refactor(plugin-sdk): dedupe session-catalog cursor paging into session-catalog-runtime
The acpx Pi and opencode session catalogs carried byte-identical
boundedLimit/encodeCursor/decodeCursor/optionalRawCursor/transcriptPage
scaffolding (~114 duplicated lines each). Move one canonical copy into the
private-local session-catalog-runtime SDK subpath both plugins already may
import, and keep the plugin-named cursor guards as direct aliases.
Net -87 production LOC; no behavior change (error strings, cursor canonical
form, and byte budgets are unchanged).
* refactor(core): replace private sleep() clones with canonical sleep helpers
managed-linux readiness polling now uses @openclaw/retry sleepWithAbort with
ref:false (preserving the clone's timer.unref behavior), and the embedded
agent runner's async-task wait uses src/utils/sleep.ts. Both clones matched
canonical semantics on real inputs (positive integer poll intervals).
The update-managed-service-handoff copy stays: it lives inside a serialized
standalone handoff script (String.raw template) that cannot import repo
modules.
* fix(plugin-sdk): keep published pre-split plugin imports loading after upgrade
Same bug class as #124041: published plugin artifacts import SDK names at
module top level, so removing them from the barrel makes the installed
plugin fail to load (voice-call/matrix doctor contracts silently never run
their migrations; whatsapp and slack channels fail outright) after a core
upgrade. Verified against the actual npm tarballs (2026.7.2-beta.7):
- openclaw/plugin-sdk/runtime-doctor: voice-call + matrix doctor contracts
import repair names (archiveLegacyStateSource, detect/repair state DB
schema, plugin install-path repair, removePluginFromConfig,
createPluginStateSyncKeyedStore) that moved to doctor-repair-runtime.
- openclaw/plugin-sdk/channel-feedback: whatsapp imports
shouldAckReactionForWhatsApp (owner policy moved in-plugin by #121257).
- openclaw/plugin-sdk/channel-outbound: slack imports
resolveChannelProgressDraftRender (render key retired by #122927).
Adds deprecated load-only bridges with named removal windows, bumps the
SDK surface budgets with comments, and locks behavior with unit tests
plus a loader fixture that fails without the bridges.
* test(plugin-sdk): cover the repair bridge in the runtime-doctor facade surface lock
Route error-bearing fan-in abandonment through the existing failure policy so deterministic pre-adoption failures cannot exceed retry limits and starve same-lane messages.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(slack): keep commentary progress on draft lane
* fix(channels): route commentary through one progress owner
Keep non-verbose commentary in Slack and Discord draft lanes while preserving one durable commentary payload when verbose progress makes those drafts yield. Freeze that owner decision for the turn so session changes apply on the next turn.
Co-authored-by: Dallin Romney <dallinromney@gmail.com>
Punchcard-Session: amber-workshop-workshop-36
* fix(channels): refresh queued commentary owner
Recompute the frozen draft-versus-durable commentary owner for every queued follow-up turn and carry that decision into final payload projection.
Punchcard-Session: amber-workshop-workshop-36
* fix(channels): clarify commentary owner opt-in
* fix(channels): preserve queued draft preambles
* fix(channels): require explicit queued progress ownership
* refactor(channels): record commentary progress owner
* fix(telegram): tolerate unscoped queued updates
* test(discord): use canonical draft fixture after rebase
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* refactor(delivery): collapse failed-row lifecycle
Replace the unshipped failure-operations platform with payload-free terminal receipts owned by existing queue boundaries. Keep bounded/permanent idempotency only for reusable or crash-ambiguous producers, move physical expiry to queue maintenance, and preserve migration and media-cleanup safety.\n\nTogether with #123410, production code is net negative by 11 lines; tests, docs, and generated protocol mirrors are accounted separately.
* fix(delivery): break state DB import cycle
* fix(delivery): classify SQLite boundary uses
* test(gateway): mark retained health fixture
* feat(computer-use): computer.act v2 action contract with capability-filtered tool
* fix(computer-use): break contract import cycle, bound model-visible elements, regen swift protocol
* test(computer-use): satisfy curly rule in schema-cap helpers
* fix(computer-use): satisfy type-aware lint on contract and tool
* test(gateway-protocol): keep connect-params suite under the line cap
Move llama.cpp chat and local embeddings onto a verified externally managed llama-server runtime. Remove the in-process native runtime, forked embedding workers, and node-llama-cpp dependency while preserving guided setup, local GGUF models, tool-capable agent runs, diagnostics, and operator docs.
Slack and Discord routed system events now retain route ownership when they enter literal-global queues, so sibling agents cannot consume them. Genuinely unbound routing remains an explicit error instead of falling back to a default agent.
* refactor(plugin-sdk): replace API baselines with diffs
* perf(plugin-sdk): bound API diff resources
* fix(plugin-sdk): isolate API diff dependencies
* fix(release): forward Plugin SDK acknowledgement
* fix(release): enforce SDK acknowledgement on publish
* chore: preserve generated-doc ignore policy
* fix(release): freeze SDK API evidence before publish
* fix(ci): satisfy SDK evidence guards
* fix(release): bind complete SDK evidence
* fix(release): authenticate plugin SDK evidence
* fix(plugin-sdk): abort interrupted API diffs
* test(ui): freeze page clock in background-tasks rail e2e
The rail transcript is compared byte-for-byte across the detail-panel
round-trip while it renders live relative ages; on slow CI runners the
second boundary ticks between the two reads (11s -> 12s) and fails the
equality assertion. Fix the page Date with Playwright setFixedTime while
keeping timers running so the tasks.list polling assertions still hold.
Repro: a 1.5s stall between the reads fails pre-fix with the exact CI
diff and passes post-fix.
* fix(scripts): drop unused export on dependency-evidence CLI main
Knip's workflow scan re-roots script references after an actions/checkout
step that sets path:, so the new trusted-tooling checkout in
openclaw-npm-release.yml stops marking this CLI as a workflow entry and
its exported main() surfaces as an unused export in check-dependencies.
Nothing imports main; the module invokes it through its own entry guard,
so the export keyword was dead surface either way.
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(sessions): isolated gateways no longer inherit HOME external session catalogs
A gateway on isolated state (custom OPENCLAW_STATE_DIR/CONFIG_PATH/OPENCLAW_HOME,
relocated home, or any named profile) listed, read, continued, archived, and
reopened the operator's real Claude Code/Codex/OpenCode/Pi sessions from the
process HOME. External catalogs now require the default install identity for
process-HOME scans: every catalog verb receives the isolation policy and rejects
HOME-fallback local targets, unknown providers fail closed unless they declare
supportsProcessHomeIsolation, and one structured warning records the skip.
Paired-node hosts and explicitly rooted stores (CLAUDE_CONFIG_DIR, CODEX_HOME,
OPENCODE_DB, Pi session dirs) keep working; default-identity gateways are
unchanged.
* fix(sessions): inject catalog HOME-isolation fact at registry construction
* chore(sdk): regenerate plugin API baselines after rebase
* chore(sdk): regenerate plugin API baselines after rebase
* fix(sessions): distinguish transient lookup failures from policy denials
Make spawned-session ownership lookup failures observable and
distinguishable from genuine tree-visibility policy denials, with
retryability classification so permanent credential/configuration
failures are not mis-framed as transient. Addresses issue #114653.
Diagnostic layer only — does not add retry (maintainer product decision)
and does not address the recency-window denial path (#114180).
Core changes:
- session-visibility-internal: add a core-private three-state lookup result
(ok / failed with retryable flag) plus classifyLookupRetryable (transport
closes/timeouts and retryable request errors => retryable; credential/auth/
unknown => non-retryable) and a shared lookupFailedDenialSuffix.
- session-visibility guard: pick the denial text by retryability so transient
failures say "retry" and permanent failures say "check gateway configuration
and credentials"; both still fail closed. Preserves the shipped public
SessionAccessResult, Set<string> checker input, and test-hook contracts.
- sessions-resolution sandbox resolver: propagate the same retryability-
classified denial; pass allowMissing: true on the speculative
sessions.resolve probe and suppress the warn for the expected "No session
found" miss so ordinary non-owned targets do not log as operational lookup
failures (reuses callGatewayResolveSession for older-gateway compatibility).
- Tests: transient + permanent error coverage for both guard and sandbox
resolver paths; classification contract locks; non-owned-miss quietness
regression; redaction through formatErrorMessage.
* fix(lint): remove unnecessary Boolean() conversion in sessions-resolution
oxlint typescript(no-unnecessary-type-conversion) flagged the redundant
Boolean() wrapper around String.includes() which already returns boolean.
Use ?? false to preserve the undefined->false semantics from the optional
chain.
* fix(sessions): keep unknown lookup failures generic
* fix(sessions): narrow ownership lookup diagnostics
* test(sessions): format visibility coverage
* fix(sessions): preserve cross-agent denials on lookup failure
A failed spawned-session ownership lookup override was relabeling the row
checker's deterministic cross-agent policy denial as a retryable lookup
failure. Retrying the spawned ownership lookup cannot make a known
agent:other:* target eligible, so the cross-visibility refusal must be
preserved as-is.
Constrain the lookup-failure override to same-agent targets by mirroring
the row checker's agent resolution: when the target resolves to a
different agent than the requester, keep the original cross-agent denial
rather than overriding it. Add regression coverage for the cross-agent
case under a failed tree-visibility lookup.
ClawSweeper review P2: preserve cross-agent policy denials on lookup
failure.
* fix(lint): avoid implicit boolean coercion in cross-agent check
Replace the `!!id` coercions in `isCrossAgentTarget` with an explicit
empty-check so the return value reads as a plain inequality. The
`no-implicit-coercion` lint rule flags `!!` on already-resolved strings;
both ids are guaranteed non-empty by the resolve-or-return-false guards
above, so the explicit form is equivalent.
* fix(sessions): preserve lookup failures for owned children
* fix(sessions): preserve ownership lookup failures
* fix(sessions): carry ownership proof through tool access
* fix(plugin-sdk): remove stale visibility imports
* fix(sessions): preserve legacy ownership fallback
* fix(sessions): block incognito status access
* fix(sessions): preserve deterministic policy denials
* fix(sessions): block incognito semantic-current status
* test(channels): isolate env preflight contracts
* test(sessions): align ownership checks with access owner
* style(sessions): format rebased access paths
* fix(sessions): require resolved owners at access checks
* test(gateway): supply explicit session owners
* test(sessions): model expected key misses precisely
* fix(sessions): scope delegated access to target owner
---------
Co-authored-by: Dallin Romney <dallinromney@gmail.com>