Keep Gateway admission sidecar-gated while surfacing retryable startup progress in TUI and Control UI.
Unify deferred startup ownership and shutdown draining so late workers, plugin services, and sidecars cannot outlive transport teardown.
Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
The finished-turn digest ('🧠 N thoughts · 💬 N notes · 🛠️ N tool calls ·
⏱️ Ns') was synthesized onto three surfaces: Slack's Block Kit card footer
(removed in #123851), Discord's final-answer subtext, and a duplicate Telegram
implementation added to match Discord.
Nothing gated it — no config key for the receipt has ever existed on any
channel — so PR #122976, which deleted it from Slack's native progress surface,
could only ever be a partial removal. Finish it: live progress surfaces keep
their in-flight indicators, and a finished turn leaves the answer with nothing
synthesized appended.
Telegram's window collapsed *into* the digest, so it now retires like every
other surface; Discord already discarded its draft after the final landed. The
core tracker keeps only the live counters Slack's working footer reads.
* fix(logging): scale RSS pressure to runtime limits
Derive Gateway RSS warning and critical thresholds from the smaller of the effective V8 heap and OS process limit. This preserves heap and rapid-growth pressure detection while avoiding routine false alerts on large heaps.
* test(logging): narrow optional heap fixture
* fix(logging): bound process memory sentinels
* test(logging): pin Bun physical memory fixture
* fix(doctor): surface legacy-config copy failures
maybeMigrateLegacyConfig swallowed every copyFile error with a bare
catch whose comment claimed EEXIST-only. EACCES, ENOSPC, and
cross-device failures were silently ignored: the operator's
~/.clawdbot/clawdbot.json exists, migration was attempted and failed,
and doctor proceeded as a clean fresh install with no config, no change
note, and no warning.
Root cause: failure collapsed into the skip-silently success shape. The
catch now rethrows anything other than EEXIST with the source/target
paths in the message; EEXIST (config already at the target) keeps its
skip semantics.
Regression: read-only target dir makes the preflight reject with
'Failed to migrate legacy config' — fails pre-fix (resolved as a clean
run); companion test proves the successful copy path still migrates.
* chore(doctor): annotate the errno assertion for the safety ratchet
* refactor(doctor): narrow the copy error without a type assertion
The assertion-safety ratchet flagged the errno cast; property narrowing
removes the assertion instead of annotating it.
Live verification of user-facing behavior is the default before landing,
with skips requiring a stated infeasibility in the PR; captured
screenshots/videos count as evidence only after the agent inspects them
in frame.
Split Signal approval routing into its concept-owned module and call the shared SDK binding helpers directly.
Reject persisted reaction targets when any allowed decision is invalid or duplicated, rather than retaining a valid subset from a corrupt transient record.
* feat(tts): add structured reply speech fields
* fix(tts): keep transcript directive parser acyclic
* fix(tts): drop unused directive fact export
* fix(outbound): preserve reply-payload metadata through response prefixes
The response-prefix rebuild spread the payload without its metadata carrier,
dropping structured speech facts (and any other payload metadata) whenever a
channel responsePrefix applied. Rebuild via copyReplyPayloadMetadata.
* fix(tts): keep voice-only sends visible when the speech runtime is cold
A structured voiceText-only send with no visible text returned unchanged from
the cold-runtime guard, so delivery normalized the empty payload to null and
the send silently vanished. The cold exit now applies the same visible
fallback as synthesis failure: explicit spoken text materializes as payload
text when no other content exists.
* fix: distinguish image inspection from delivery
Clarify that image-reading tools only load private model context, prefer native Codex image inspection when available, and preserve explicit outbound tool media across Claude CLI loopback runs.
Co-authored-by: pash <pash@openai.com>
* fix: propagate CLI vision capability
* chore: refresh CI after main fix
---------
Co-authored-by: pash <pash@openai.com>
A config file whose JSON5 root parsed to a scalar (null, a number, a
bare string — the classic truncated/clobbered file) hit a special case
in loadConfigFromContext that returned {} with a snapshot marked
valid: true. Three failures in one: the process silently ran with
defaults while the operator's real config (channels, auth, allowlists)
disappeared; the valid snapshot could promote the corrupt file's
fingerprint as lastKnownGood, poisoning the clobber-recovery machinery
that exists to catch exactly this; and the load path contradicted the
snapshot path, which correctly reported the same bytes invalid.
Root cause: an early-return that collapsed invalid input into the
empty-config success shape. Deleted; a scalar root now flows into
validateConfigObjectWithPlugins, fails schema validation, records a
valid:false snapshot, and throws INVALID_CONFIG — same as an array
root, same as the snapshot path, handled by doctor's invalid-config
recovery.
Regression: new io.scalar-root.test.ts (null/number/string roots all
throw INVALID_CONFIG; snapshot agrees invalid) — all 3 fail pre-fix.
Updated the compat warning-fingerprint test that relied on null-root
loading as defaults.
Restrict exact config identity trust to prepared runtime generations and force startup convergence to verify against a fresh authoritative plugin snapshot.
#124844 made resolvePluginMetadataSnapshot synthesize workspacePluginRootPresent
itself with a process-memoized fs.existsSync probe whenever a caller did not
supply it. That fact is owned by the prepared-model-runtime lease, which resolves
it only for real agent runs (resolveWorkspacePluginRootPresence) and passes it
explicitly, so the probe silently turned an opt-in fast path into the default for
unrelated control-plane callers.
Gateway startup config validation is one of them. It never asserts the fact, so it
began projecting the published lifecycle graph instead of loading a fresh one, and
the projection derives configFingerprint from that graph rather than a real load.
Startup convergence rewrites the persisted plugin index between the two reads that
form the migration checkpoint identity, so the pre-convergence read projected while
the post-convergence read loaded. The two pluginMigrationFingerprint values differed
and the gateway refused readiness with "OpenClaw plugin migration inputs changed
during startup convergence", failing every config-patch restart.
Deleting the probe restores one owner for the fact and a net-negative production
diff. The agent-side hunks of #124844 are untouched: they still pass the fact
explicitly from the lease.
Root cause: admission fact manufactured outside its lifecycle owner.
Owner boundary: src/agents/prepared-model-runtime-lease.ts owns workspace
plugin-root presence; src/plugins/plugin-metadata-snapshot.ts only consumes it.
Production LOC: -18.
Fixes red main ci-gate: QA Smoke memory-dreaming-sweep, matrix-restart-resume,
matrix-post-restart-room-continue.