* feat(onboarding): recommend plugins and skills from installed apps
Scan installed macOS apps during classic onboarding (TCC-free), gather
candidates from official catalogs + ClawHub search, let the configured
model pick genuine matches, and offer an opt-in multiselect install step.
Adds a device.apps node-host command (default-off sharing, Android-parity
envelope) so remote gateways can request a paired Mac's inventory, and a
wizard.appRecommendations kill switch. Custom setup-inference completions
no longer inherit the 32-token verification-probe output cap.
* feat(onboarding): recommend apps in guided flow
* fix(onboarding): harden app recommendations against ClawHub self-promotion
Third-party ClawHub skills are never pre-selected regardless of model tier
(publisher-controlled listing text reaches the matcher prompt and could
promote itself); their labels now say they install third-party code.
Installed-app scans follow symlinked .app bundles. Matcher output stays
bounded by the resolved model's own maxTokens budget (documented invariant).
* fix(onboarding): key official catalog candidates by resolved plugin id
Real catalog entries are package manifests without a top-level id; keying the
candidate map and channel/provider classification by entry.id collapsed the
whole official catalog into one undefined-keyed entry, so no official plugin
or channel was ever recommended. Regression test runs against the bundled
catalogs.
* fix(onboarding): satisfy lint, types, deadcode, and migration gates
Split the guided-onboarding test into a self-contained custodian suite to stay
under max-lines. Narrow app-recommendation exports (drop dead node-payload
normalizer, unexport internal types/helpers, route candidate tests through the
public API), replace map-spread with a helper, unexport device.apps result
types, add installedAppsSharing to node-host migration expectations, cast the
wizard multiselect mock, and regenerate the docs map.
* test(onboarding): register new live test in the shard classifier
Both flakes (CI run 29573675642, macos-swift) were the same mechanism:
fixture timestamps anchored to wall clock at test start, raced against
the optimistic user-echo timestamp captured later at send time. The
pending-run drain (clearPendingRunIfAssistantMessagePresent ->
assistantHapticEvent(after:)) only matches assistant rows timestamped
at or after the user echo, and the fake transports' default
waitForRunCompletion returns .unavailable, so this comparison is the
only drain path in both tests.
ChatHapticsTests "durable assistant failure fires run failed": the
durable failure row was stamped test start + 1000ms. Under CI load a
>1s stall between fixture creation and send() left the row permanently
older than the user echo, so .runFailed never fired for either
parameterized case. Fix: HapticsTestTransport now takes a history
closure and the test stamps the failure row at requestHistory time,
which is always post-send by event order.
ChatStreamReplayTests "reconnect mid-run converges via history
refetch": the completed transcript rows were stamped test start +
100/900ms; the scripted payload has no inFlightRun/sessionInfo, so the
foreground refetch cannot drain and only the pending-run owner's
timestamp comparison can. Bootstrap plus send taking >900ms made the
assistant row older than the echo and the run never drained. Fix:
anchor the reconnect transcript to a timestamp captured after the send
completed.
Proof: injecting a 1.5s/1.2s stall before send reproduced both CI
failures exactly (timeout after ~16.5s/16.2s, both haptics cases
failing); with the fixes the tests pass even with the stalls injected.
Full ChatHapticsTests + ChatStreamReplayTests suites green; both fixed
tests stressed 10x with zero failures.
* test: serialize concurrent vitest mock resolution in the non-isolated runner
Root-causes the subagent-orphan-recovery flake (main run 29566318167, shard
agentic-agents-core-subagents): "includes last human message in resume when
available" and "adds config change hint..." received the bare resume template
because prod code called a different readSessionMessagesAsync mock instance
than the one the test configured.
Mechanism: vitest's BareModuleMocker.resolveMocks has no in-flight guard.
pendingIds is cleared only after all parallel resolveId RPCs settle, and every
registration re-invalidates the mock module node. In a shared isolate:false
worker, leaked async work from an earlier file (a real-timer dynamic import)
triggers a fetchModule while the next file's vi.mock registrations are still
pending, starting a second concurrent resolution pass over the same
pendingIds. The slower pass re-registers each manual mock and wipes the
already-evaluated mock module mid-import-chain: importers evaluated before the
wipe (the test file's own binding, subagent-orphan-recovery.test.ts:8) keep
factory instance A while later importers (subagent-orphan-recovery.ts via
test line 16) instantiate a fresh instance B. Only inline-factory mocks split;
vi.hoisted-stable mocks are immune, matching the CI failure signature exactly.
Fix: OpenClawNonIsolatedRunner installs a coalescing wrapper around
moduleRunner.mocker.resolveMocks (once per worker) so concurrent callers share
one pass and registration happens exactly once before any awaiting import
proceeds.
Proof:
- Deterministic repro (poison file leaving a 1ms recurring dynamic-import
timer + resolveId jitter on session-transcript-readers) reproduced the exact
CI assertion failures at lines 755/772 on the unfixed runner and passes 5/5
with the fix.
- New unit tests cover coalescing, idempotent install, and fresh passes.
- Stress: 10x subagent-orphan-recovery.test.ts and 5x the full
agentic-agents-core-subagents file set (63 files, 1220 tests) at
OPENCLAW_VITEST_MAX_WORKERS=6, all green.
* test: requeue mock ids queued during an in-flight resolveMocks pass
Review follow-up to the resolveMocks serialization pin: upstream snapshots the
pendingIds contents at pass start and reassigns the static to [] at the end,
so ids queued during the pass's RPC window land in the abandoned array. Pure
coalescing would silently drop those registrations (upstream's racy second
pass previously processed them). The wrapper now captures the queue reference
before each pass, requeues ids pushed past the processed count, and drains
until the queue is empty, so every coalesced caller's vi.mock registration is
applied before its fetch proceeds.
Proof: new unit test "registers ids queued while a pass is in flight before
callers proceed"; deterministic zombie+jitter repro still passes 5/5; full
agentic-agents-core-subagents file set green at OPENCLAW_VITEST_MAX_WORKERS=6.
* test: chain resolveMocks passes per caller instead of sharing one pass
The coalescing pin regressed shard agentic-agents-core-auth
(models-config.providers.auth-provenance.test.ts, runs 29570918219 and
29571513381): sharing one in-flight pass broke the mocker's freshness
invariant. Upstream gives every resolveMocks caller a pass that starts at or
after its call, so ids the caller queued (vi.doUnmock before dynamic imports
in that file's beforeAll/beforeEach) are always registered before its fetch
proceeds. A coalesced caller could instead ride a pass snapshotted before its
ids were queued and import with mock state unresolved. Reproduced locally on a
cold transform cache: the file then loaded the real provider-auth warm worker
plus a live oauth-manager refresh guarded by a 120s withRefreshCallTimeout
(the 105-142s stalls and the "Test timed out in 120000ms" CI failure), and the
real resolveProviderSyntheticAuthWithPlugin returned undefined (the
mode:"none" assertion failure at line 348).
The wrapper now chains each caller onto its own sequential pass. This keeps
both invariants: serialization (a pass queued behind an in-flight one sees the
cleared queue and no-ops, so a snapshot is never registered or its mock
modules invalidated twice - the original subagent-orphan-recovery disease) and
freshness (each caller's pass starts at or after its call). Abandoned-id
requeue is preserved so ids pushed during a pass's RPC window are registered
by the next chained pass instead of dropped.
Proof:
- Cold-cache auth shard (37 files, 528 tests): failed 105-142s with the
coalescing pin (also with a coalescing-only variant), passed 20s with the
unfixed runner, passes 3/3 in ~20-30s with the chained pin.
- Long-timer instrumentation pinpointed the stall: provider-auth warm worker
spawn plus oauth-manager withRefreshCallTimeout delay=120000.
- Original zombie+jitter orphan-recovery repro still passes 5/5.
- Warm agentic-agents-core-subagents (63 files, 1220 tests) and auth shard
green at OPENCLAW_VITEST_MAX_WORKERS=6; wrapper unit tests updated to
assert serialization, freshness, requeue, idempotent install.
The installation hit Blacksmith's backing-disk cap because sticky keys
embedded PR numbers and content hashes, minting a new disk per PR and
per input change until every mount 429-failed fleet-wide. node-deps
(#109752) and ext-boundary (#109804) were already re-keyed; this
converts the last two minters and audits the third suspect.
Keys changed:
- Vitest fs transform cache: `vitest-fs-v2-<pr-N|protected>-...` ->
the one existing `vitest-fs-v2-protected-<os>-<arch>-node-<ver>`
disk. Entries are content-hash keyed (vitest sha1 over
id+content+NODE_ENV+version+env config), so cross-PR sharing is safe
by construction: PRs now mount the protected snapshot directly
(read-only, commit false) and the separate PR-only seed mount is
deleted. Writers (non-PR shard writer + scheduled warm run) keep
explicit commit true. Disk count: 1 + O(open PRs) -> 1.
- Gradle: `gradle-v1-<task>-<pr-N|protected>-<hashFiles(deps)>` ->
`gradle-v2-<task>`. Task scope stays (light ktlint must not seed
heavy build lanes); PR number and dependency hash leave the key. The
dependency hash moved to an in-job fingerprint marker that makes the
non-PR writer rebuild its snapshot cold when inputs change, bounding
disk growth; PR mounts are read-only and safely reuse stale
snapshots because Gradle caches are content-addressed. Disk count:
O(tasks x PRs x dependency bumps) -> O(tasks) (7 today).
- Docker builder (useblacksmith/setup-docker-builder): audited, out of
scope. The action owns its key internally and always uses the repo
name only (src/setup_builder.ts getStickyDisk), so it is already one
disk per installation repo and cannot mint per-PR disks.
PR warm-path behavior changes:
- Vitest: PRs lose only PR-local warm entries for files the PR itself
changed (previously persisted on the pr-N disk between pushes);
changed files re-transform once per push, unchanged files still hit
the protected snapshot. PRs touching transform inputs
(lockfile/tsconfig/package.json) run cold per push since the
generation wipe is now local to the discarded clone.
- Gradle: dependency-bump PRs get warmer (stale-but-valid protected
snapshot instead of a cold fresh key); other PRs are unchanged.
Deleted `.github/workflows/pr-cache-cleanup.yml`: it existed for the
per-PR cache layer (#109425) and only deleted GitHub actions/cache
archives, which GitHub's own LRU/TTL eviction already handles for the
remaining fork/Windows per-PR archive paths.
Guards: pinned the O(1) vitest key + non-PR commit gate, added a
Gradle per-task key/writer/fingerprint guard, added a repo-wide scan
asserting no useblacksmith/stickydisk key ever contains
github.event.pull_request.number or hashFiles(), and replaced the
cleanup-workflow assertions with a stays-deleted check.
* fix(line): adopt durable ingress drain with ack gated on spool write
LINE acked webhooks 200 before processing events detached, so a crash or
dispatch failure silently lost inbound messages. Events now enqueue their raw
JSON into the channel ingress queue before the webhook responds; dispatch,
retry, dead-letter, and completion tombstones run through the core drain.
The in-memory replay guard is deleted; tombstone retention (30d/4096)
strictly covers its old 10min/4096 window, and message-id keys preserve the
guard keyspace so redeliveries under changed webhookEventIds still dedupe.
Autoreview: secret scanner false-positive on HMAC test fixtures
(test-channel-secret); full manual review of all five files + test matrix
performed instead. Part of the #109657 fleet adoption program (wave 1).
* style(line): oxfmt pass on drain adoption files
* fix(line): cap repeated ingress drain deliveries
* fix(line): preserve ingress lifecycle bounds
* style(line): satisfy ingress lint gates
* fix(sqlite): bound fallback mount classification
* test(sqlite): cover mount probe failure policy
* test(sqlite): use tracked mount probe fixtures
* perf(sqlite): keep mount result import type-only
* style(sqlite): format mount probe result
* perf(sqlite): defer mount process module loading
* perf(sqlite): avoid eager mount parser dependency
* refactor(sqlite): isolate lock error detection
* revert: keep sqlite lock errors colocated
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(slack): adopt durable ingress drain across Bolt and relay with logical twin guard
Slack events now enqueue their raw envelope durably before the transport ack
on all three modes: the Bolt receiver wrap covers Socket Mode and HTTP
(event_id = Events API event_id), and relay frames enqueue before the router
ack (keyed by logical team:channel:ts since router delivery-id redelivery
stability is undocumented). Dispatch, retry, dead-letter, and 30d/20k
tombstones run through the core drain; claimed relay events retry until the
relay source reattaches after a restart. The in-memory + persistent inbound
delivery caches and the app-mention race machinery are deleted.
One dedupe layer deliberately survives: Slack emits both a message and an
app_mention event (distinct event_ids) for a single mention post, so a new
claim-based dispatch guard keyed on logical (team, channel, ts) collapses the
twins — commit at turn adoption, release on gated/abandoned dispatch so the
surviving twin can re-run the same gate. 24h/20k retention mirrors the
retired persistent cache.
Slack monitor test state now scopes a fresh OPENCLAW_STATE_DIR per test;
persisted guard keys otherwise dedupe unrelated fixture messages that reuse
ts values. Five reaction tests in monitor.tool-result.test.ts are flaky on
clean origin/main under full-suite load (verified 2-of-3 baseline runs);
tracked separately.
Autoreview: secret scanner false-positives on token-shaped test fixtures
(three files); full manual review performed instead, which caught and fixed
the twin-guard regression. Part of the #109657 fleet adoption program.
* fix(slack): remove unused ingress exports
* chore(slack): prune max-lines baseline
* docs(plugin-sdk): refresh API baseline hash
* test(slack): type dispatch mock argument
* test(slack): use managed temp root