* fix(agents): keep not_required delivery status when completion_announced_at exists for expectsCompletionMessage=false runs
createDeliveryFromTypedColumns unconditionally overrode status to
'delivered' when completion_announced_at was present, even for runs
where expects_completion_message is false. This produced a contradictory
delivery state where a run that does not require completion delivery
was marked as delivered.
Only set status to 'delivered' when expects_completion_message is true.
For not_required runs that still have completion_announced_at recorded
(for informational purposes), preserve announcedAt without overriding
the status.
* test(agents): cover tainted delivery rows
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(android): add Wear proxy protocol
Introduce a bounded, versioned phone/watch RPC contract and wire its test, lint, and build gates before either runtime endpoint lands.
Co-authored-by: Sebastian Schubotz <git@sibbl.de>
* test(android): lock Wear wire names
Cover every supported RPC method and event discriminator so phone and watch cannot silently drift.
Co-authored-by: Sebastian Schubotz <git@sibbl.net>
* fix(android): bound Wear JSON depth
Reject excessively nested Data Layer envelopes before kotlinx.serialization can recurse through them.
* fix(android): enforce Wear depth on encode
Keep outbound messages inside the same JSON nesting contract enforced by the decoder.
* fix(android): preflight Wear payload depth
Traverse arbitrary JSON payloads iteratively before serialization so deeply nested local trees fail safely.
---------
Co-authored-by: Sebastian Schubotz <git@sibbl.de>
Co-authored-by: Sebastian Schubotz <git@sibbl.net>
* feat(migrate): add Hermes memory-only import and a shared memory-import core
* feat(onboarding): offer detected memory imports during CLI setup and guided onboarding
* feat(ui): show a first-run memory-import offer in Control UI onboarding mode
* feat(linux-app): open the first-run dashboard in onboarding mode
* feat(macos): add the onboarding memory-import page
* docs: document the onboarding memory-import page across surfaces
* chore(i18n): translate onboarding memory-import strings for control-ui and native locales
* refactor: keep memory-import internals unexported for deadcode gates
* fix(ci): resolve lint findings in onboarding memory import
* chore(i18n): refresh native inventory after lint refactor
* fix(ci): skip updater artifacts in the unsigned Linux companion PR lane
* chore(i18n): reconcile locale artifacts after rebase
The child wrote its pid with fs.writeFileSync, which makes the file visible at
open() (0 bytes) before the content lands. Under CI load the existsSync-gated
reader in waitForFile could observe the empty file, so readFileSync returned ''
and Number.parseInt yielded NaN, making isProcessAlive(NaN) false and failing the
pre-kill liveness assertion (checks-node-compact-small-6 flake). Write the pid to
a temp file and rename it into place so the path only appears with full content.
* perf(ci): carry extension boundary artifacts on a Blacksmith sticky disk
The boundary lane spends ~113s of its 214s rebuilding plugin-sdk
declaration artifacts that an actions/cache was meant to carry — but
that cache is evicted so fast under the repo quota that even its prefix
restore never hits. Same-repo runs now keep the artifacts on a sticky
disk with a coarse toolchain key (source changes intentionally hit a
stale snapshot: restored mtimes predate the fresh checkout, so the
mtime-based freshness predicate always routes into the incremental
rebuild — a stale snapshot can never false-skip the compile). Fork PRs
keep the GitHub cache path and can never write repository-global
snapshots. Seed/restore round-trip verified locally with rsync -aR
layout mirroring.
* perf(ci): restore boundary artifacts for the lint lane too
check-lint's shard runner spawns the same plugin-sdk boundary artifact
prep the boundary lane snapshots (~72s of its wall clock); it now
mounts the shared sticky read-only-by-convention (only the boundary
lane seeds) and restores before the run step.
* fix(ci): make the lint boundary-sticky mount strictly read-only
Autoreview caught a first-hydration race: with commit if-missing, a
cold-key lint job racing the boundary lane could commit an empty,
marker-less snapshot that if-missing then never repairs. commit false
makes lint a pure reader; only the boundary lane can seed.
* fix(agents,skills): keep session-identity and experience-review transcript truncation UTF-16 safe
normalizeCliContextValue (cli-output.ts) and formatSkillExperienceReviewTranscript
(experience-review-prompt.ts) both used raw .slice(0, N) / .slice(-N) to truncate
user-facing strings. When a truncation boundary falls inside a surrogate pair (emoji),
the resulting string contains a lone surrogate — encodeURIComponent throws URIError,
JSON serialisers may reject it, and downstream display surfaces can render garbage.
Replace .slice(0, 200) with truncateUtf16Safe in normalizeCliContextValue.
Replace .slice(0, 6_000) with truncateUtf16Safe and full.slice(-tailBudget) with
sliceUtf16Safe(full, tailStart) in formatSkillExperienceReviewTranscript.
Follows the pattern established by #98644, #102470, #101728 and the broader
UTF-16 safety sweep already applied across the codebase.
* chore: re-trigger CI after PR body update
* test(agents,skills): construct transcripts exceeding 60k chars, verify old slices dangle
Both experience-review fixtures now exceed EXPERIENCE_REVIEW_MAX_TRANSCRIPT_CHARS
so the truncation branch actually fires. Pre-condition assertions prove the old raw
.slice(0,6000) / .slice(tailStart) produce isolated surrogates; the production
functions (formatSkillExperienceReviewTranscript, formatCliOutputError) do not.
cli-output test also adds pre-condition check showing normalizeCliContextValue
with raw .slice(0,200) would split a surrogate pair.
* test: tighten UTF-16 truncation coverage
Co-authored-by: 毛宇豪0668001457 <mao.yuhao@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(ios): render plugin approvals as native Allow/Deny notifications
Extend the iOS approval handling from exec to plugin approvals so the native
push the gateway now sends (#108505) shows a phone notification with Allow/Deny.
Extract a shared ApprovalNotificationBridge with exec/plugin wrappers owning
their own kind constants and category; tag the parsed prompt with its kind;
register the plugin category and route plugin pushes in the app delegate; add a
.plugin case that maps the plugin presentation (title/description/pluginId/
severity) into the existing prompt and widens the exec-only validation, current,
cache-restore, and resolve guards to accept plugin; parameterize the dialog copy
by kind. Watch snapshot publication stays exec-only; the phone notification still
mirrors to the watch via WatchConnectivity. Reuses the unified approval.get/
approval.resolve RPCs (already kind-generic); no server or protocol changes.
* chore(i18n): sync native inventory after rebase
* chore(ios): drop accidentally committed local Version.xcconfig build override
* fix(ios): sync Apple i18n catalog and remove dead approval-bridge methods
Sync apps/ios/Resources/Localizable.xcstrings for the new plugin-approval
strings (native inventory sync alone did not cover the Apple catalog).
Remove the superseded per-bridge registerCategory variants and the redundant
exec parseResolvedPush wrapper flagged by the iOS Periphery dead-code scan;
registration uses ApprovalNotificationBridge.registerCategories and the shared
parse helpers.
* chore(ios): drop generated SwiftSources filelist; reconcile native i18n inventory
* chore(ios): drop generated Xcode project from tracking
Codex generated OpenClaw.xcodeproj for its simulator test run and the initial
apps/ios directory commit force-added it; xcodegen regenerates it in CI, so it
must not be tracked (matches main).
* chore(ios): reconcile native i18n inventory after rebase
PR #108283 started deriving a durable channel source-turn id for gateway
chat.send turns carrying originatingChannel/originatingTo overrides, while
the gateway's pre-created user-turn recorder keys the persisted turn by run
id. The same PR's admission guard then throws 'channel restart recovery
requires source-keyed user-turn admission' for every such turn — breaking
qa-lab suite runs, ACP-bridge flows, and relay integrations (reproduced
live on the codex harness chat-image probe; text-only fails identically).
Internal-origin turns (gateway chat.send stamps the internal channel as the
ingress provider) carry run ids, not provider message ids; gateway turns
already own restart via fingerprint admission and client-side idempotent
retries. Skip the mint for internal ingress at both derivation sites so
admission takes the source-less persist path, while real channel ingress
keeps the new source-keyed recovery. Also surface the full agent.wait
result in the codex harness live test error (how this was found).
Live proof: gateway-codex-harness.live.test.ts chat-image probe on a
Blacksmith Testbox with a real OpenAI key failed with the admission error
twice on main and passes admission with this fix.
* fix(shell-env): cap login shell env probe cache with eviction
The loginShellEnvProbeCache Map keys include the exec function reference
and exec environment, which can vary across calls. A 64-entry eviction
cap prevents unbounded growth.
* refactor(shell-env): share bounded probe cache insertion
Co-authored-by: 陈志强0668000989 <31802073+wings1029@users.noreply.github.com>
* fix(shell-env): retain active probe cache entries
Co-authored-by: 陈志强0668000989 <31802073+wings1029@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* perf(ci): unblock parallel oxlint shards and gate the lint-lane i18n verify
check-lint is the slowest static lane (median 252s): the control-UI
i18n verify runs 86s unconditionally and the three oxlint shards run
serially (89s) because the parallel gate demands 48GB of memory — a
local-Mac threshold that Blacksmith's 32GB 16 vCPU class can never
meet. CI now parallelizes shards at 8+ cores / 24GB+, and the lint case
reuses the existing run_control_ui_i18n manifest gate so diffs that
cannot affect generated catalogs skip the verify while oxlint always
runs.
* chore(ci): print oxlint shard concurrency decision
The lint-lane validation run still executed shards serially on a 16
vCPU runner; one log line exposes the resolved concurrency and the
host resources the decision saw.
* fix(xai): cancel realtime reconnect backoff on close
* test(xai): cover reconnect after bridge close
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The gateway already broadcasts stream:"plan" agent events with typed
{step, status} snapshots; all three clients dropped them. Control UI now
renders an expandable checklist bar above the composer (run-scoped,
cleared on run lifecycle); the shared iOS/macOS chat UI mounts a
Liquid Glass pill (iOS 26 glassEffect, material fallback) between the
message list and composer; Android adds an expandable pill above the
composer bridged through ChatController/NodeRuntime/MainViewModel.
All parsers accept typed and legacy string steps, enforce the
single-in_progress invariant, and scope snapshots and clears to the
owning run. Locale artifacts, Android resources, and the iOS string
catalog carry generated translations for the pill strings.
333c4f9a61 and 244f4965dc landed six exports with no production
consumers, failing the hard-zero deadcode gate on every PR: a dead
re-export block in restart-handoff (the contract module's real importer
uses it directly), three result/mode types only referenced inside their
own modules, and two supervision symbols whose only consumers were their
tests. The types and helpers stay defined for the in-flight supervisor
work to re-export alongside real consumers; the supervision test now
proves mode resolution through the public isGatewayExternallySupervised
surface.
Without OP_LOAD_DESKTOP_APP_SETTINGS=false and OP_BIOMETRIC_UNLOCK_ENABLED=false,
op 2.35 on macOS reads the 1Password desktop app's settings even on the
service-account path and can block on a per-PID App Data Protection dialog,
hanging the broker until timeoutMs on Mac gateway hosts.
* feat(mac): mascot accessory pack — nightcap, grad cap, hat-tip exit
New accessory channel for the shared mascot: sleepy (and auto-dozing)
Clawd wears a floppy blue nightcap; the final onboarding page tops the
celebration with a graduation cap whose tassel swings with the body;
and leaving the working mood plays a hat-tip — claw to the brim, small
bow, hard hat lifts away. Headwear drawing moves to its own canvas
extension file.
* fix(mac): one hat at a time and no phantom hat tips
The working hard hat (including its hat-tip exit) suppresses any mood-
or consumer-requested accessory until it is fully gone, and the hat-tip
plays only when the hat actually seated — a working state cancelled
mid-don exits through the normal entrance instead of synthesizing a
full hat just to lift it.
* chore(i18n): refresh native inventory lines for mascot view
* fix(discord): open latest activity widget on fallback
* chore: remove release-owned changelog entry
* fix(discord): order activity fallback by delivery
* fix(discord): require atomic widget delivery updates
* test(discord): type activity delivery fixtures
* test(discord): model missing atomic state support
* fix(discord): order activity fallback by message snowflake
* docs(discord): clarify activity fallback order
* fix(feishu): honor abortSignal in drive comment reply retry loop
The inter-attempt delay in monitor.comment.ts used a bare setTimeout and
never received the owning monitor account's AbortSignal, so an account
teardown (ws disconnect, webhook stop, config reload) left the in-flight
comment reply retry running for up to ~6s after the rest of the lifecycle
settled. delayMs now wraps waitForAbortableDelay from async.ts and the
abortSignal is threaded through fetchDriveCommentContext →
resolveDriveCommentEventTurn → handleFeishuCommentEvent →
createFeishuDriveCommentNoticeHandler, sourced from
monitorSingleAccount.
The retry loop checks the waitMs return value and the abortSignal flag,
breaking out of the next 1s tick instead of running the full 6×1s poll
budget. Sibling surface monitor.bot-identity.ts already uses the same
pattern; #104431 explicitly skipped the comment-monitor retry because
it is a result-predicate poll, not an error retry, and this fix is
orthogonal to that decision.
Not tested: live Feishu tenant, real bot identity, real drive comment,
and account restart. requestFeishuOpenApi itself does not accept
AbortSignal — that is a separate, larger refactor and is out of scope.
* test(feishu): prove comment retry abort
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(ui): web delights — empty-state mascots, first-reply confetti, composer drag-catch
Three mascot-powered moments in the Control UI: the plugins page and
channels hub show a sleepy Clawd when truly empty and a curious one on
a search miss; the very first successful assistant reply in a browser
earns a single confetti burst in the mascot palette (localStorage
once-flag, reduced-motion aware); and dragging files over the new-
session composer makes the welcome mascot tease with an open mouth,
then snap a catch on drop — upload behavior untouched.
* fix(ui): satisfy strict types and knip for the confetti module
Tuple index under noUncheckedIndexedAccess gets an anchored fallback,
the once-flag helper stays module-internal (knip counts only production
consumers), and its coverage moves to the public fireFirstReplyConfetti
path in jsdom using the shared storage mock.