A linked worktree whose base predates (or carries) wrapper changes relative
to origin/main previously hit a hard refusal, even though the canonical
checkout held exactly the trusted origin/main wrapper the refusal message
told the operator to go run by hand. When the canonical checkout is clean
and byte-identical to fetched refs/remotes/origin/main, exec it with a loud
stderr notice instead; advisory dev-wrapper opt-in keeps precedence, and
the refusal remains when no anchor-matching wrapper exists on disk.
Also records the squash-merged stacked-branch rebase gotcha
(git rebase --onto origin/main <landed-branch>) in the PR maintainer skill.
* 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>
checks-node-core-tooling-2 failed on main (run 31943910358) with
'expected 1786879225197 to be >= 1786879225197.2246': the repaired output
mtime landed a fraction of a millisecond below the input it had to clear.
isArtifactSetFresh repairs output mtimes to exactly ceil(newestInput), so
it leaves zero headroom for sub-millisecond write rounding or lagging
metadata on CI filesystems. When the repair lands at or below its input
the mtime fast path never engages, and every later invocation in that
checkout falls back to re-hashing every input byte -- the expensive path
this repair exists to avoid, gating the d.ts emit that is the slowest
build-all phase (25.9s of a 61s build).
Neither macOS APFS nor an idle Linux ext4 Testbox reproduces the
shortfall in 300 runs, so the repair now clears the newest input by a
whole millisecond instead of matching it. The assertion pins that
headroom, making the test deterministic where it was previously
load-dependent (fails 615 vs 616 without the fix).
* fix(computer-use): unblock the macOS live-rig proof flow
The rig ran its operator CLI and its proof runner from one state dir, so both
shared one device identity. A paired operator device is pinned to the scopes of
its first connect, and `nodes list` connects first for `node.pair.list`
(operator.pairing); the proof runner then needs operator.write, which is a scope
upgrade the gateway never approves silently and which no rig client can approve
for itself. The proof runner is a GATEWAY_CLIENT/BACKEND client, so on a
loopback auth-none gateway it is admitted unpaired with the scopes it asks for:
giving the CLI its own `cli-state` identity is enough, and `agent-state` now
never accumulates a pairing row.
`nodes list` also read `node.list` through the plain CLI client while
`nodes status`/`describe` used the diagnostics ladder. On any gateway where the
CLI must pair, the unfiltered list silently dropped connected/commands/
computerUse and `--connected` failed outright, so the documented rig gate could
not confirm the node. Both call sites now use `callNodeDiagnosticsGatewayCli`.
Docs drop the `devices approve <requestId>` instruction, which was circular:
that invocation is its own new device identity.
* test(cli): share the runtime-log formatter across nodes CLI e2e files
The extracted diagnostics-auth file stringified captured log arguments directly, which the type-aware core lint stripe rejects (no-base-to-string). Move the existing formatter into the shared node test helpers instead of duplicating it.
* feat(workers): run device sessions from Gateway bundles
Install the current Gateway bundle before a device environment becomes ready, verify it at attach and tunnel boundaries, launch only from the immutable namespaced bundle directory, and retire stale environments for idempotent reprovisioning. Remove the local execution mode and preserve the node-local build claim only as temporary inventory metadata for the final projection/cleanup slice.
* docs(runners): record Gateway bundle cutover
* test(ci): repair runner validation fixtures
# Conflicts:
# src/scripts/test-projects.test.ts
* fix(workers): surface outdated node recovery
Keep legacy runner inventory diagnostic-only while exposing the update-and-reconnect action through node, environment, provider, placement, and Control UI surfaces.
* fix(workers): reject legacy inventory with recovery
* fix(workers): bundle worker deploy closure
* test(workers): close bundle cutover gates
* fix(workers): compose browser runtime at build
* fix(workers): satisfy bundle cutover gates
* fix(workers): route temp runtime through infra
* docs(workers): align bundle host guidance
* fix(ui): fence outdated session destinations
* fix(cua-computer): prove Linux X11 live vertical
* test(computer-use): authenticate isolated Linux rig
* fix(gateway): refresh computer use after node approval
* refactor(cua-computer): resolve the plugin manifest by static import
* fix(gateway): break plugin runtime import cycle
* fix(computer-use): bind live rig to committed helpers
* 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