Address Greptile review feedback on PR #41361:
1. Fire before_compaction/after_compaction hooks in the overflow recovery
path (run.ts) when engine owns compaction — same asymmetry that was
fixed for the /compact command path.
2. Add 3 tests for compactEmbeddedPiSession with ownsCompaction=true:
- Verifies hooks fire with sentinel -1 values
- Verifies after_compaction skipped on failed compaction
- Verifies hook exceptions are caught without aborting compaction
Match legacy path semantics: compactEmbeddedPiSessionDirect only reaches
the after_compaction hook after session.compact() succeeds. Gate the
engine-owned path on result.ok && result.compacted so subscribers don't
receive after_compaction for failed compactions.
- Remove tokenCount from before_compaction in engine-owned path since we
don't have actual transcript token counts (was incorrectly set to
budget). tokenCount is optional in the hook type.
- Resolve workspaceDir via resolveUserPath() to match the normalized
path used in compactEmbeddedPiSessionDirect.
CodeRabbit correctly flagged that passing messageCount: 0 and
compactedCount: 0 is misleading — subscribers would interpret it as an
empty session. Use -1 as a sentinel to signal 'unavailable' since the
engine-owned path doesn't load the transcript. Subscribers can read the
sessionFile directly if they need exact counts. Also pass tokenCount
(budget) to before_compaction for context.
Bug 1: contextEngine.compact() in the overflow retry loop (run.ts) could
throw and kill the entire agent run. The LegacyContextEngine is safe
(internal try/catch), but plugin-provided engines are not. Wrap the call
in try/catch to convert thrown errors into a failed CompactResult.
Bug 2: When a context engine sets ownsCompaction: true, Pi's built-in
auto-compaction is disabled, but before_compaction/after_compaction hooks
are only fired from the built-in compaction path. Plugin hook subscribers
are silently broken. Fire hooks in compactEmbeddedPiSession() when the
engine owns compaction.
Also fixes pre-existing test failure in compact.hooks.test.ts where the
buildEmbeddedExtensionFactories mock returned [] instead of { factories: [] }.
Bug 1 (high): replace fixed sleep 1 with caller-PID polling in both
kickstart and start-after-exit handoff modes. The helper now waits until
kill -0 $caller_pid fails before issuing launchctl kickstart -k.
Bug 2 (medium): gate enable+bootstrap fallback on isLaunchctlNotLoaded().
Only attempt re-registration when kickstart -k fails because the job is
absent; all other kickstart failures now re-throw the original error.
Follows up on 3c0fd3dffe.
Fixes#43311, #43406, #43035, #43049
tsx, jiti, ts-node, ts-node-esm, vite-node, and esno were not recognized
as interpreter-style script runners in invoke-system-run-plan.ts. These
runners produced mutableFileOperand: null, causing invoke-system-run.ts
to skip revalidation entirely. A mutated script payload would execute
without the approval binding check that node ./run.js already enforced.
Two-part fix:
- Add tsx, jiti, and related TypeScript/ESM loaders to the known script
runner set so they produce a valid mutableFileOperand from the planner
- Add a fail-closed runtime guard in invoke-system-run.ts that denies
execution when a script run should have a mutable-file binding but the
approval plan is missing it, preventing unknown future runners from
silently bypassing revalidation
Fixes GHSA-qc36-x95h-7j53
In trusted-proxy mode, enforceOriginCheckForAnyClient was set to false
whenever proxy headers were present. This allowed browser-originated
WebSocket connections from untrusted origins to bypass origin validation
entirely, as the check only ran for control-ui and webchat client types.
An attacker serving a page from an untrusted origin could connect through
a trusted reverse proxy, inherit proxy-injected identity, and obtain
operator.admin access via the sharedAuthOk / roleCanSkipDeviceIdentity
path without any origin restriction.
Remove the hasProxyHeaders exemption so origin validation runs for all
browser-originated connections regardless of how the request arrived.
Fixes GHSA-5wcw-8jjv-m286
On macOS, launchctl bootout permanently unloads the LaunchAgent plist.
Even with KeepAlive: true, launchd cannot respawn a service whose plist
has been removed from its registry. This left users with a dead gateway
requiring manual 'openclaw gateway install' to recover.
Affected trigger paths:
- openclaw gateway restart from an agent session (#43311)
- SIGTERM on config reload (#43406)
- Gateway self-restart via SIGTERM (#43035)
- Hot reload on channel config change (#43049)
Switch restartLaunchAgent() to launchctl kickstart -k, which force-kills
and restarts the service without unloading the plist. When the restart
originates from inside the launchd-managed process tree, delegate to a
new detached handoff helper (launchd-restart-handoff.ts) to avoid the
caller being killed mid-command. Self-restart paths in process-respawn.ts
now schedule the detached start-after-exit handoff before exiting instead
of relying on exit/KeepAlive timing.
Fixes#43311, #43406, #43035, #43049