Skip to content

feat(safety): Emergency Stop for desktop automation (#4255)#4600

Merged
M3gA-Mind merged 43 commits into
tinyhumansai:mainfrom
CodeGhost21:feat/desktop-safety-4255
Jul 16, 2026
Merged

feat(safety): Emergency Stop for desktop automation (#4255)#4600
M3gA-Mind merged 43 commits into
tinyhumansai:mainfrom
CodeGhost21:feat/desktop-safety-4255

Conversation

@CodeGhost21

@CodeGhost21 CodeGhost21 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds an Emergency Stop kill switch for desktop automation (issue Desktop automation safety previews, confirmations, history, and emergency stop #4255, first slice): a persistent top-right control that instantly halts all running/queued automation and blocks any further automated action until the user resumes.
  • Fail-closed enforcement at two chokepoints — the tinyagents approval middleware refuses external-effect tool calls while halted (fires even when the approval gate is disabled), and accessibility_input_action refuses desktop input (panic_stop exempt).
  • New Rust emergency_stop domain: a process-global OnceLock switch, RPCs openhuman.emergency_{stop,resume,status}, boot install; engaging also stops the accessibility session and cascade-denies pending approvals (best-effort).
  • AutomationHalted/AutomationResumed events bridged to an automation_halt socket broadcast (client_id: "system"); React UI: Redux safetySlice, emergencyApi, a top-right Emergency Stop button (hidden while halted) + an Automation halted banner with Resume, boot-time hydration, and i18n safety.* across all locales.
  • In-memory only — the halt resets on restart (persistence is an explicit follow-up).

Problem

Desktop control can click, type, delete, send, and purchase on the user's behalf. Non-technical users need one always-available way to instantly stop everything and be sure nothing else runs until they say so. This implements the emergency-stop acceptance criterion of #4255: "Emergency stop can cancel pending actions and prevents further queued actions from running until the user resumes."

Solution

  • EmergencyStop mirrors ApprovalGate's install idiom: try_global() → None (CLI/headless, not installed) never blocks — fail-open; engaged → fail-closed.
  • emergency_stop(reason, source) sets the AtomicBool before any side effects; the a11y-session stop and cascade-deny are best-effort and can never un-set the flag or fail the RPC. SeqCst ordering with a fail-closed torn-read fallback (reviewed).
  • Both enforcement chokepoints read is_engaged_global(). The approval middleware is installed unconditionally, so the tool refusal holds even with OPENHUMAN_APPROVAL_GATE=0.
  • Frontend state is driven by the RPC responses, a boot-time emergency_status hydration, and the automation_halt socket broadcast. The Stop button hides while halted so the banner's Resume is the sole control (no dual Stop+Resume).

Design decisions / tradeoffs: in-memory halt (resets on restart) for this slice; no confirmation dialog before stopping (a kill switch shouldn't be gated); backend (backend-alphahuman) intentionally unchanged — this is a local Tauri-core control.

Submission Checklist

  • Tests added or updated (happy path + failure/edge) — 15 Rust unit tests + 1 JSON-RPC E2E (emergency_stop_roundtrip_over_rpc); Vitest for the slice, API client, automation_halt socket handler, boot hydration, button (incl. hide-when-halted), and banner. Failure paths covered (RPC throws → local halt; malformed socket payload dropped; fail-open when uninstalled).
  • Diff coverage ≥ 80% — tests were added specifically for the changed lines (socket handler, boot hydration, cascade-deny loop, socket-bridge handle). The CI diff-cover gate is authoritative; final number verified there.
  • Coverage matrix updated — N/A this slice: the Emergency Stop row is deferred to the follow-up PR (see Related); not added here to avoid touching the curated feature-ids.json matrix-sync mapping (epic Enforce unit tests, feature tests, and documentation for core foundation  #773).
  • All affected feature IDs listed under ## Related — new feature; see Related.
  • No new external network dependencies introduced — none; core-local switch, no backend calls.
  • Manual smoke checklist updated — N/A this slice: deferred to the follow-up PR; not a release-cut surface yet (see Related).
  • Linked issue closed via Closes #NNN — N/A: intentionally non-closing — first slice of epic Desktop automation safety previews, confirmations, history, and emergency stop #4255, referenced as "Part of Desktop automation safety previews, confirmations, history, and emergency stop #4255" so the epic stays open (see Related).

Impact

  • Platform: desktop (macOS/Windows/Linux). No mobile/web/CLI behavior change (CLI/headless never installs the switch → never blocks).
  • Security: net-positive — a fail-closed kill switch that refuses outward actions while engaged.
  • Performance: negligible — an AtomicBool load at the two chokepoints.
  • Migration/compat: none — in-memory state, no schema/config changes, backend untouched.

Related


AI Authored PR Metadata

Linear Issue

  • Key: N/A (GitHub-only)
  • URL: N/A

Commit & Branch

  • Branch: feat/desktop-safety-4255
  • Commit SHA: a5cbd50c2

Validation Run

  • pnpm --filter openhuman-app format:check — prettier applied to all changed frontend files (style(emergency) commits)
  • pnpm typecheck — changed files typecheck clean; repo-wide has pre-existing unrelated failures (@xyflow/react, rehype-highlight) not from this PR
  • Focused tests: pnpm test src/store/safetySlice src/services/api/emergencyApi src/components/safety (all green); cargo test -p openhuman emergency (15 pass, parallel)
  • Rust fmt/check (changed): cargo fmt --check + cargo check clean
  • Tauri fmt/check — N/A: no app/src-tauri changes

Validation Blocked

  • command: full desktop GUI enforcement of the tool-middleware chokepoint
  • error: staging account's AI provider key is unset, so the agent can't invoke a tool in-chat
  • impact: low — enforcement proven at the RPC layer (screen_intelligence_input_action returns blocked, reason:"emergency_stop" only while halted; clears on resume) and by unit tests

Behavior Changes

  • Intended behavior change: a new user-invocable Emergency Stop that halts automation and blocks external-effect tools + accessibility input until resume.
  • User-visible effect: a top-right Emergency Stop button (hidden while halted) and an Automation halted banner with Resume.

Parity Contract

  • Legacy behavior preserved: no change when the switch is not engaged; approval gate, accessibility, and chat/LLM turns behave exactly as before.
  • Guard/fallback/dispatch parity: fail-open when uninstalled; best-effort side effects never affect the primary flag; panic_stop bypasses the guard.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this
  • Resolution: N/A

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added an Emergency Stop control to immediately halt desktop automation.
    • Added a top-right Emergency Stop button (desktop shell) plus an “automation halted” banner with a Resume action.
    • Automation now remains blocked until explicitly resumed, with halt/resume state synchronized and preserved through app startup hydration.
    • Added user-facing failure messaging and retry behavior for stop/resume.
    • Added translated safety messaging across supported languages.
  • Tests

    • Added/expanded coverage for Emergency Stop, Resume, halted-state synchronization, socket event delivery, and end-to-end RPC roundtrips.

Closes : #4255

CodeGhost21 and others added 21 commits July 6, 2026 16:10
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… slice 1)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nyhumansai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nyhumansai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inyhumansai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ansai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nyhumansai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsai#4255)

Add HaltState serde type and EmergencyStop OnceLock singleton mirroring
the ApprovalGate install pattern. Types: engaged flag, reason, timestamp,
source. State: init_global/try_global, engage/clear/snapshot, is_engaged_global.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inyhumansai#4255)

Register all_emergency_registered_controllers and all_emergency_controller_schemas
in src/core/all.rs (next to approval). Install EmergencyStop::init_global() in
jsonrpc.rs boot sequence next to ApprovalGate::init_global.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ile halted (tinyhumansai#4255)

Also introduces a shared crate-visible EMERGENCY_TEST_GUARD in state.rs so all
tests touching the process-global EmergencyStop (ops, middleware, screen
intelligence) serialize against the same mutex under parallel execution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tinyhumansai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…event (tinyhumansai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…up (tinyhumansai#4255)

- EmergencyStopButton: call emergencyStop() with no argument so the halt
  reason stays undefined and the banner falls back to safety.haltedBody
  instead of displaying the literal string "user". Error-path setHalt no
  longer hardcodes reason: 'user'. onClick renamed to handleClick and
  wrapped in void to avoid passing an async fn as the event handler.
- socketService: add automation_halt handler tests (engaged=true →
  setHalt, engaged=false → clearHalt, malformed payload → silent drop).
- Extracted hydrateEmergencyState helper from AppShellDesktop boot effect
  for testability; App effect is now a one-liner. Tests cover success,
  not-engaged, and thrown emergencyStatus paths.
- Removed unused safety.stopConfirm key from en.ts and all 13 locale
  files; pnpm i18n:check exits 0 with no missing or extra keys.
- AutomationHaltedBanner.test: replaced brittle getByText('Automation
  halted') with role+data-analytics-id assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nyhumansai#4255)

The bottom-right FAB overlapped the chat composer's Quick/Reasoning
controls. Pin it to the top-right (clear of the composer and sidebar;
macOS traffic lights sit top-left) and hide it while halted so the
AutomationHaltedBanner's Resume is the sole control then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@CodeGhost21
CodeGhost21 requested a review from a team July 6, 2026 19:19
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a process-global emergency-stop control spanning Rust state, RPC, enforcement, websocket events, Redux synchronization, desktop safety UI, localized messages, tests, and design documentation.

Changes

Emergency Stop Feature

Layer / File(s) Summary
Domain events and web bridge
src/core/event_bus/*, src/openhuman/channels/providers/web/*
Adds halt/resume domain events and publishes them as automation_halt websocket events.
Emergency stop core state and RPC
src/openhuman/emergency_stop/*, src/core/all.rs, src/core/jsonrpc.rs, tests/json_rpc_e2e.rs
Adds global halt state, stop/resume/status RPCs, controller registration, startup initialization, and lifecycle tests.
Tool and input blocking
src/openhuman/tinyagents/middleware.rs, src/openhuman/screen_intelligence/ops.rs
Blocks external-effect tools and accessibility input while halted, except for panic_stop.
Frontend safety state and API
app/src/store/*, app/src/services/api/*, app/src/services/safety/*, app/src/services/socketService.ts
Adds Redux halt state, emergency RPC calls, response normalization, boot hydration, socket synchronization, and tests.
UI controls and shell wiring
app/src/App.tsx, app/src/components/safety/*
Adds the emergency-stop button, halted banner with resume behavior, and desktop-shell integration.
Safety translation keys
app/src/lib/i18n/*
Adds emergency-stop and halted-state strings across supported locales.
Design and plan docs
docs/superpowers/{plans,specs}/*
Documents the architecture, enforcement points, RPC flow, frontend integration, testing plan, and implementation checklist.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Desktop
  participant CoreRPC
  participant EmergencyStop
  participant EventBus
  participant Socket
  participant Redux
  Desktop->>CoreRPC: emergency_stop(reason)
  CoreRPC->>EmergencyStop: engage and deny pending activity
  EmergencyStop->>EventBus: publish AutomationHalted
  EventBus->>Socket: emit automation_halt
  Socket->>Redux: dispatch setHalt
  Redux->>Desktop: render halted banner
  Desktop->>CoreRPC: emergency_resume()
Loading

Suggested labels: feature, rust-core

Poem

I’m a rabbit with a button bright,
I halt the bots when things aren’t right.
Rust sends a signal, sockets hop,
Redux says, “Automation, stop!”
Resume when the path is clear—
Safety carrots all around here. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding Emergency Stop safety controls for desktop automation.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5cbd50c26

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/src/services/socketService.ts Outdated
Comment thread src/openhuman/channels/runtime/startup.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (5)
app/src/services/api/emergencyApi.ts (1)

12-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding debug logging for this new RPC flow.

emergencyStop/emergencyResume/emergencyStatus introduce a new call path but log nothing themselves (callers like EmergencyStopButton/AutomationHaltedBanner only log on the error path). A stable-prefixed debug log on invocation (method + reason) would aid correlating a stop/resume attempt with the resulting core state across the boot/hydrate/socket flows in this feature.

As per path instructions, "On new or changed flows, add verbose debug logging with stable prefixes, correlation fields, and no secrets or full PII; incomplete changes are those lacking logging."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/services/api/emergencyApi.ts` around lines 12 - 25, Add
stable-prefixed debug logging to the new RPC helpers in emergencyApi so each
invocation is visible even on success. In emergencyStop, emergencyResume, and
emergencyStatus, log the method being called and the reason when present before
calling callCoreRpc, using a consistent prefix that can be correlated with
EmergencyStopButton and AutomationHaltedBanner flows. Keep the log verbose
enough for tracing across boot/hydrate/socket paths, but avoid secrets or full
PII.

Source: Path instructions

app/src/components/safety/AutomationHaltedBanner.tsx (1)

22-30: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider richer logging for this new safety flow.

Only the failure path logs (single line, no correlation id). Per path instructions, new/changed flows should have verbose debug logging with correlation fields.

As per path instructions: "On new or changed flows, add verbose debug logging with stable prefixes, correlation fields, and no secrets or full PII; incomplete changes are those lacking logging."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/safety/AutomationHaltedBanner.tsx` around lines 22 - 30,
The onResume emergency flow in AutomationHaltedBanner only logs a terse failure
message and lacks the verbose, correlated debug logging required for new/changed
safety paths. Update the onResume callback around emergencyResume() to add
stable-prefixed debug logs for the attempt, success, and failure paths, and
include correlation fields already available in this component/context (without
secrets or full PII); keep clear context in the existing error log or replace it
with structured logging using the same identifiers.

Source: Path instructions

app/src/components/safety/EmergencyStopButton.tsx (2)

26-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing verbose/correlated debug logging for a new safety flow.

The failure path logs only console.error('[emergency] stop failed', err). As per path instructions, "On new or changed flows, add verbose debug logging with stable prefixes, correlation fields, and no secrets or full PII; incomplete changes are those lacking logging." Consider adding a correlation id (e.g., a per-click token) and logging both the attempt and outcome (success/failure) with that stable prefix, to make it easier to trace emergency-stop attempts in support/debugging scenarios.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/safety/EmergencyStopButton.tsx` around lines 26 - 30, The
EmergencyStopButton safety flow only logs the failure, so add verbose debug
logging around the full emergency-stop attempt with the existing [emergency]
prefix. In EmergencyStopButton, generate a per-click correlation token and log
both the attempt and the outcome in the try/catch path, including stable fields
like the token and source while avoiding secrets or full PII. Update the current
console.error in the catch block and add a matching success log so
support/debugging can trace a single stop request end to end.

Source: Path instructions


22-31: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No guard against duplicate concurrent stop requests.

handleClick has no in-flight/disabled state, so rapid clicks before the Redux halted flag flips (and the button unmounts) could fire multiple emergencyStop() calls. The backend is likely idempotent for engage, but consider disabling the button (or ignoring re-entrant clicks) while the request is pending to avoid redundant RPCs and console noise.

💡 Proposed guard
+  const [pending, setPending] = useState(false);
   const handleClick = useCallback(async () => {
+    if (pending) return;
+    setPending(true);
     try {
       const state = await emergencyStop();
       dispatch(setHalt({ reason: state.reason, source: state.source, since: state.engaged_at_ms }));
     } catch (err) {
       dispatch(setHalt({ source: 'user' }));
       console.error('[emergency] stop failed', err);
+    } finally {
+      setPending(false);
     }
-  }, [dispatch]);
+  }, [dispatch, pending]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/safety/EmergencyStopButton.tsx` around lines 22 - 31, The
EmergencyStopButton handleClick flow allows duplicate concurrent emergencyStop()
requests because there is no in-flight guard before Redux setHalt unmounts the
button. Update EmergencyStopButton to track a pending state around handleClick
and either disable the button or ignore re-entrant clicks until the request
settles, while keeping the existing dispatch(setHalt(...)) and error handling
behavior intact.
app/src/lib/i18n/es.ts (1)

7030-7037: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Inconsistent register: "Reanude" uses formal usted imperative, rest of file uses informal .

The rest of the file consistently uses informal imperative forms (e.g., "Confirma", "Guarda cambios", "Envía"). haltedBody uses "Reanude" (formal usted imperative of reanudar), while resume correctly uses "Reanudar" (infinitive, register-neutral). This is an inconsistency that will stand out to Spanish-speaking users.

🐛 Proposed fix
-  'safety.haltedBody': 'Toda la automatización de escritorio está detenida. Reanude cuando esté listo.',
+  'safety.haltedBody': 'Toda la automatización de escritorio está detenida. Reanuda cuando estés listo.',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/i18n/es.ts` around lines 7030 - 7037, Update the Spanish copy in
the i18n mapping for safety.haltedBody so its wording matches the file’s
informal tú register used elsewhere. The issue is the formal “Reanude” wording;
change the sentence to use an informal imperative/phrasing consistent with keys
like safety.resume, safety.emergencyStop, and the other translations in this
file. Keep the meaning the same while aligning tone across the safety strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/safety/EmergencyStopButton.tsx`:
- Around line 8-16: The emergency stop flow in EmergencyStopButton currently
treats a failed `emergency_stop` RPC the same as a confirmed halt by calling
`setHalt({ source: 'user' })` in the catch path. Update the button’s click
handling and the related safety slice state so a requested halt is tracked
separately from a confirmed `automation_halt` from the core, using a distinct
unconfirmed/error state or retry indicator instead of marking the system halted
locally on RPC failure. Keep the Resume banner logic tied only to confirmed halt
state so Stop and Resume are not hidden prematurely.

In `@app/src/services/socketService.ts`:
- Around line 467-491: The automation_halt handler in socketService should fail
closed for ambiguous payloads instead of treating a missing engaged field as
false. Update the this.socket.on('automation_halt', ...) logic to require a real
boolean engaged before calling setHalt or clearHalt, and drop/ignore objects
like {} or {reason: 'x'} the same way invalid shapes are handled. Add or extend
coverage in socketService.events.test.ts for the object-without-engaged case so
the handler does not silently dispatch clearHalt().

In `@docs/superpowers/plans/2026-07-06-emergency-stop.md`:
- Around line 21-22: The locale path references in the i18n guidance are
incorrect and point to a non-existent locales directory. Update the plan text to
reference the actual translation files under app/src/lib/i18n, using the
existing locale module names such as en.ts, ar.ts, de.ts, and the rest of the
listed language files. Keep the rest of the i18n requirement intact, and make
sure the path wording in the emergency-stop doc matches the repo’s real
structure so implementers can find the right symbols and files.
- Around line 726-743: The emergency-stop fast-fail in wrap_tool currently
returns before the approval audit trail is updated, so halted tool calls are not
recorded as aborted. Update the existing halt check in wrap_tool to also write
the audit-trail entry when crate::openhuman::emergency_stop::is_engaged_global()
is true, using the same aborted/blocked path that ApprovalGate::try_global()
handling uses, and keep the fail-closed return behavior unchanged.

In `@docs/superpowers/specs/2026-07-06-emergency-stop-design.md`:
- Line 27: The web bridge reference is pointing to the wrong module path, which
will send implementers to the wrong place. Update the guidance to reference the
actual bridge module in the web provider stack, and keep the surrounding context
tied to the controller registry and approval-surface event bridging so readers
can still locate the right RPC/event flow. Use the existing symbols like
controller registry and ApprovalSurfaceSubscriber to preserve the implementation
trail, but ensure the bridge path points to the correct web event bus module.

In `@src/openhuman/emergency_stop/ops.rs`:
- Around line 56-78: The cascade-deny work in cascade_deny_pending is using
synchronous ApprovalGate::list_pending and ApprovalGate::decide calls, which can
block a Tokio worker thread. Move this best-effort approval draining into a
blocking context such as spawn_blocking or block_in_place, and keep the existing
error handling/logging while preserving the same behavior for pending approvals.

In `@src/openhuman/emergency_stop/state.rs`:
- Around line 53-70: The `EmergencyStopState::engage` and
`EmergencyStopState::clear` updates to `engaged` and `info` are not atomic as a
pair, so concurrent calls can leave `snapshot()` seeing inconsistent state. Fix
this by making the state transition in each method happen under the same
mutex-protected critical section used for `info`, and update `engaged` and
`info` together in `engage`, `clear`, and any related read path in `snapshot()`
to preserve a consistent pair of values.

In `@src/openhuman/screen_intelligence/ops.rs`:
- Around line 325-374: The emergency-stop tests can leak the process-global
engaged state if they panic before reaching the final clear call. In both
input_action_blocked_while_emergency_engaged and
panic_stop_passes_even_while_emergency_engaged, add a Drop-based cleanup guard
immediately after EmergencyStop::init_global() so stop.clear() always runs even
on assertion failure or unwrap panic. Keep the existing test logic intact, but
ensure the guard owns the cleanup for the EmergencyStop state.

In `@src/openhuman/tinyagents/middleware.rs`:
- Around line 3935-3946: Make the emergency-stop test cleanup panic-safe by
ensuring the process-global state is always reset even if an assertion fails.
Update emergency_guard_blocks_when_engaged to use a small Drop-based guard
around EmergencyStop::init_global()/engage so stop.clear() runs during
unwinding, rather than relying on the final explicit clear call after the
assertions.

In `@tests/json_rpc_e2e.rs`:
- Around line 1195-1261: The emergency-stop roundtrip test leaves the
process-global EmergencyStop engaged if any assertion panics before the final
resume call. Update json_rpc_emergency_stop_roundtrip_over_rpc to use an
RAII/scoped cleanup guard (or equivalent defer-style cleanup) around the
openhuman.emergency_stop/openhuman.emergency_resume sequence so resume is
guaranteed to run even on failure; place the cleanup near the test setup and
keep the existing stop/status/resume assertions in the same function.

---

Nitpick comments:
In `@app/src/components/safety/AutomationHaltedBanner.tsx`:
- Around line 22-30: The onResume emergency flow in AutomationHaltedBanner only
logs a terse failure message and lacks the verbose, correlated debug logging
required for new/changed safety paths. Update the onResume callback around
emergencyResume() to add stable-prefixed debug logs for the attempt, success,
and failure paths, and include correlation fields already available in this
component/context (without secrets or full PII); keep clear context in the
existing error log or replace it with structured logging using the same
identifiers.

In `@app/src/components/safety/EmergencyStopButton.tsx`:
- Around line 26-30: The EmergencyStopButton safety flow only logs the failure,
so add verbose debug logging around the full emergency-stop attempt with the
existing [emergency] prefix. In EmergencyStopButton, generate a per-click
correlation token and log both the attempt and the outcome in the try/catch
path, including stable fields like the token and source while avoiding secrets
or full PII. Update the current console.error in the catch block and add a
matching success log so support/debugging can trace a single stop request end to
end.
- Around line 22-31: The EmergencyStopButton handleClick flow allows duplicate
concurrent emergencyStop() requests because there is no in-flight guard before
Redux setHalt unmounts the button. Update EmergencyStopButton to track a pending
state around handleClick and either disable the button or ignore re-entrant
clicks until the request settles, while keeping the existing
dispatch(setHalt(...)) and error handling behavior intact.

In `@app/src/lib/i18n/es.ts`:
- Around line 7030-7037: Update the Spanish copy in the i18n mapping for
safety.haltedBody so its wording matches the file’s informal tú register used
elsewhere. The issue is the formal “Reanude” wording; change the sentence to use
an informal imperative/phrasing consistent with keys like safety.resume,
safety.emergencyStop, and the other translations in this file. Keep the meaning
the same while aligning tone across the safety strings.

In `@app/src/services/api/emergencyApi.ts`:
- Around line 12-25: Add stable-prefixed debug logging to the new RPC helpers in
emergencyApi so each invocation is visible even on success. In emergencyStop,
emergencyResume, and emergencyStatus, log the method being called and the reason
when present before calling callCoreRpc, using a consistent prefix that can be
correlated with EmergencyStopButton and AutomationHaltedBanner flows. Keep the
log verbose enough for tracing across boot/hydrate/socket paths, but avoid
secrets or full PII.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a6e55f0-e5f4-4da5-aafb-49ed61ace5b3

📥 Commits

Reviewing files that changed from the base of the PR and between f492493 and a5cbd50.

📒 Files selected for processing (47)
  • app/src/App.tsx
  • app/src/components/safety/AutomationHaltedBanner.test.tsx
  • app/src/components/safety/AutomationHaltedBanner.tsx
  • app/src/components/safety/EmergencyStopButton.test.tsx
  • app/src/components/safety/EmergencyStopButton.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/services/__tests__/socketService.events.test.ts
  • app/src/services/api/emergencyApi.test.ts
  • app/src/services/api/emergencyApi.ts
  • app/src/services/safety/hydrateEmergencyState.test.ts
  • app/src/services/safety/hydrateEmergencyState.ts
  • app/src/services/socketService.ts
  • app/src/store/index.ts
  • app/src/store/safetySlice.test.ts
  • app/src/store/safetySlice.ts
  • app/src/test/test-utils.tsx
  • docs/superpowers/plans/2026-07-06-emergency-stop.md
  • docs/superpowers/specs/2026-07-06-emergency-stop-design.md
  • src/core/all.rs
  • src/core/event_bus/events.rs
  • src/core/event_bus/events_tests.rs
  • src/core/jsonrpc.rs
  • src/openhuman/channels/providers/web/event_bus.rs
  • src/openhuman/channels/providers/web/mod.rs
  • src/openhuman/channels/runtime/startup.rs
  • src/openhuman/emergency_stop/mod.rs
  • src/openhuman/emergency_stop/ops.rs
  • src/openhuman/emergency_stop/schemas.rs
  • src/openhuman/emergency_stop/state.rs
  • src/openhuman/emergency_stop/types.rs
  • src/openhuman/mod.rs
  • src/openhuman/screen_intelligence/ops.rs
  • src/openhuman/tinyagents/middleware.rs
  • tests/json_rpc_e2e.rs

Comment thread app/src/components/safety/EmergencyStopButton.tsx
Comment thread app/src/services/socketService.ts
Comment thread docs/superpowers/plans/2026-07-06-emergency-stop.md Outdated
Comment thread docs/superpowers/plans/2026-07-06-emergency-stop.md
Comment thread docs/superpowers/specs/2026-07-06-emergency-stop-design.md Outdated
Comment thread src/openhuman/emergency_stop/ops.rs
Comment thread src/openhuman/emergency_stop/state.rs
Comment thread src/openhuman/screen_intelligence/ops.rs Outdated
Comment thread src/openhuman/tinyagents/middleware.rs
Comment thread tests/json_rpc_e2e.rs
CodeGhost21 and others added 2 commits July 7, 2026 01:16
…ansai#4255)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tomic state, coverage (tinyhumansai#4255)

- socketService: drop automation_halt payloads lacking a boolean engaged
  (a malformed broadcast can no longer silently clear an active halt).
- EmergencyStopButton: do not mark halted locally when the stop RPC fails
  (no false sense of safety); log + leave button for retry.
- state.rs: write the engaged flag inside the info mutex so snapshot never
  sees an inconsistent (flag, info) pair; is_engaged() stays lock-free.
- ops.rs: run the blocking cascade-deny via spawn_blocking off the worker.
- Extract emergency_halt_denial helper + unit test, and schemas handler/arm
  test — cover the changed lines the >=80% Rust diff-cover gate flagged.
- Panic-safe RAII cleanup guards in the middleware, screen-intel, and e2e tests.
- Add [emergency] debug logging (button/banner/api); es.ts informal tú; doc paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai coderabbitai Bot added the agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. label Jul 6, 2026
AutomationHaltedBanner.test.tsx queries the retry indicator via
getByRole('status', { name: /could not resume/i }), but the ARIA `status`
role is name-from-author — its accessible name comes from aria-label/
aria-labelledby, not text content — so a `<span role="status">` with only
text has no accessible name and the query fails (Frontend Checks red). Add
aria-label={t('safety.resumeFailed')} so the live region is properly named
for assistive tech and the test resolves it.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Maintainer fix pass:

CI — Frontend Checks (was RED): AutomationHaltedBanner.test.tsx failed both preserves halt and surfaces a retry message when emergencyResume fails and clears the stale retry indicator on a new halt cycle with Unable to find role="status" and name /could not resume/i. Root cause: the ARIA status role is name-from-author — its accessible name is computed from aria-label/aria-labelledby, not text content — so the retry <span role="status"> (text-only) had no accessible name and getByRole('status', { name: … }) couldn't resolve it. Fix: added aria-label={t('safety.resumeFailed')} to that span in AutomationHaltedBanner.tsx — a genuine a11y improvement (named live region) that also satisfies the test. (621b3cd5f)

CI — Rust Core Coverage (was RED): the sole failure was openhuman::memory_queue::worker::tests::run_once_reschedules_reembed_backfill_jobs_that_defer (1 failed / 11,774 passed) — a module this PR does not touch, in a run showing heavy parallel-SQLite contention (whatsapp_data corruption/lock churn). It passed on the other coverage runs I ran today. Treating it as a parallel-contention flake; the push above re-triggers the job to re-verify. Not the known Bus-error pattern, but an unrelated-module single-test flake, not a defect in this PR's code.

CodeRabbit review (14:15 CHANGES_REQUESTED) — both actionable items already fixed in current HEAD:

  • L40 (Major) stale resumeFailed across halt cycles: the component already has useEffect(() => { if (!halted) setResumeFailed(false); }, [halted]), which clears the flag on any halt-lift path (including the socket-driven external clear), not just onResume. Addressed.
  • L62 (Minor) empty-string reason: the component already uses {reason || t('safety.haltedBody')} (||, which falls back on ''), not ??. Addressed (CodeRabbit's own comment appended "✅ Addressed in …").

Letting PR CI re-verify (no local matrix run).

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

@coderabbitai review

Both actionable items from the last review are already resolved in current HEAD: the stale-resumeFailed reset is handled by the useEffect(!halted → setResumeFailed(false)), and the reason fallback uses || (empty-string safe). The latest push adds an aria-label to the resume-failed role="status" region (fixes the Frontend Checks failure and names the live region). Please re-review against current HEAD.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== AutomationHaltedBanner.tsx ==="
fd AutomationHaltedBanner.tsx app/src/components/safety --exec cat -n {}

Length of output: 4840


🏁 Script executed:

#!/bin/bash
rg -n "resumeFailed" app/src/lib/i18n/en.ts

Length of output: 285


@M3gA-Mind Verified against current HEAD — both items check out:

  • Stale resumeFailed reset: the useEffect(() => { if (!halted) setResumeFailed(false); }, [halted]) (Lines 47-49) fires on any path that clears halted, including the socket-driven external clearHalt(), not just the local onResume success path.
  • Empty-string reason fallback: Line 61 uses reason || t('safety.haltedBody'), which correctly falls back for both undefined and ''.
  • New aria-label fix: Line 68 now sets aria-label={t('safety.resumeFailed')} on the role="status" span, giving the live region an accessible name (previously it had none, since ARIA status name is computed from aria-label/aria-labelledby, not text content) — this resolves the getByRole('status', { name: … }) failures in the test suite and is a genuine accessibility improvement.

Kicking off a fresh review pass against current HEAD.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

@coderabbitai review

Status update for this fix pass:

  • Frontend Checks: now green — the AutomationHaltedBanner role="status" accessible-name fix (aria-label) from commit 621b3cd5f resolved the two failing tests.
  • Rust Core Coverage: red on an unrelated testopenhuman::memory_queue::worker::tests::run_once_reschedules_reembed_backfill_jobs_that_defer (1 failed / 11,782 passed). This module is untouched by this PR. The job runs the full openhuman::* lib suite in one parallel binary (because this PR edits src/openhuman/mod.rs, which widens the changed-module coverage filter to bare openhuman), and the run logs show heavy parallel-SQLite contention (whatsapp_data temp-DB corruption). The reembed test deferred correctly (status/attempts/reschedule assertions all passed) — only its last_error string didn't contain the expected reason, consistent with a DB-contention defer reason under load, not this PR's code. Re-running the failed job.
  • CodeRabbit (14:15 CHANGES_REQUESTED): both items are already resolved in HEAD — the stale-resumeFailed reset via useEffect(!halted → setResumeFailed(false)), and the empty-string-safe {reason || t('safety.haltedBody')} fallback. Please re-review against current HEAD.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "=== AutomationHaltedBanner.tsx ==="
fd AutomationHaltedBanner.tsx app/src/components/safety --exec cat -n {}

Length of output: 4840


@M3gA-Mind Verified against current HEAD — all three items check out:

  • Stale resumeFailed reset: Lines 47-49 useEffect(() => { if (!halted) setResumeFailed(false); }, [halted]) correctly resets on any path that lifts the halt, including a socket-driven clearHalt().
  • Empty-string reason fallback: Line 61 {reason || t('safety.haltedBody')} correctly falls back for both undefined and ''.
  • aria-label on the retry status region: Lines 66-68 confirm aria-label={t('safety.resumeFailed')} is present on the role="status" span, giving it an accessible name.

The unrelated memory_queue::worker coverage flake and the widened openhuman filter explanation are noted — nothing actionable there for this review. Kicking off a fresh review pass against current HEAD.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Resolve i18n conflicts in 13 non-English locale files: main added the
privacy status/disclosure keys (tinyhumansai#4437/S3) at the same tail position where this
PR adds the Emergency Stop safety keys (tinyhumansai#4255). Both key blocks are additive and
in disjoint namespaces (safety.* vs privacy.*), so keep BOTH — a union merge.
Verified each locale matches en.ts key-for-key (67 safety+privacy keys, no
missing/extra).
Review (tinyhumansai#4600): the ops.rs unit tests engaged the process-global kill switch and
cleaned up via a manual emergency_resume() at the end. An assertion panic between
engage and resume would leak the engaged switch into later tests in the same
binary. Add the RAII ClearEmergencyOnDrop guard (already used in schemas.rs) right
after the EMERGENCY_TEST_GUARD lock so the switch is cleared on drop, including on
panic unwind. Mirrors the guard M3gA-Mind/CodeRabbit asked for on the sibling
middleware/screen_intelligence tests.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 14, 2026
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Maintainer fix pass — merged fresh upstream/main and worked the review queue. Pushed e5ddf7c31 (merge) + bfd0c59b0 (test hardening).

Merge conflicts resolved (13 i18n locale files): main added the privacy status/disclosure keys (#4437/S3) at the same tail position where this PR adds the Emergency-Stop safety.* keys. Disjoint namespaces, both additive → union merge (kept both blocks). Verified every locale matches en.ts key-for-key (67 safety.*+privacy.* keys each, 0 missing/extra) so i18n:check parity holds.

Fixed by me:

  • emergency_stop/ops.rs tests (Issue: M3gA-Mind — panic-safe guard) — the three ops tests engaged the process-global switch and reset via a manual emergency_resume(); a panic between engage and resume would leak engaged into later tests in the same binary. Added the RAII ClearEmergencyOnDrop guard right after the EMERGENCY_TEST_GUARD lock (same guard already used in schemas.rs), so cleanup runs on drop incl. panic unwind.

Verified already addressed in the current head (resolving these stale threads):

  • Halt broadcast payload nesting (Codex P1 + YellowSnnowmann Blocker, socketService.ts) — the handler now reads halt fields from obj.args (the WebChannelEvent envelope) with a top-level fallback, and drops payloads whose engaged isn't a boolean. ✓
  • Halt registration skipped on desktop (Codex P2, channels/runtime/startup.rs) — register_automation_halt_subscriber() is now also registered on the always-run serve-boot path (core/jsonrpc.rs:2346), which runs on a web-chat-only desktop that skips start_channels. Idempotent (Once-guarded; register_automation_halt_subscriber_is_idempotent test). ✓
  • Scheduled jobs run while halted (Codex P1, cron/scheduler.rs) — execute_job_with_retry now fail-closes on is_engaged_global() at the outermost dispatch, covering JobType::Shell/Flow that bypass the tinyagents middleware. ✓
  • Resume-failure clears state (Codex P2, AutomationHaltedBanner.tsx) — clearHalt() now fires only on a CONFIRMED resume; the catch path leaves the banner up. ✓
  • Audit trail for halted denials (CodeRabbit Major + M3gA-Mind Major, middleware.rs / spec) — reconciled via CodeRabbit's own second option: the spec's Task 7 carries an explicit "Audit-trail note (deferred)" (plan line 745) stating the halt short-circuit intentionally does not write an aborted approval-audit row in this slice (it needs a new non-prompting gate API that doesn't exist today — intercept_audited would prompt), that the refusal is already surfaced via tracing::warn! + the AutomationHalted event/automation_halt broadcast, and that the audit write is tracked as a follow-up. The slice is now internally consistent (spec ↔ code). I did not implement the audit write: adding a new record_aborted-style surface to the security-critical approval store, unable to run the local test matrix, is riskier than the sanctioned spec reconciliation.

Replies (no code change):

  • cron e.to_string()format!("{e:#}") (YellowSnnowmann, question) — safe. That value is the detail/observability tuple element (scheduler.rs:1003), not the user-facing body (which is classify_agent_anyhow_for_user(&e)); the code comment notes the loopback-unreachable classifier + observability pipeline key on the full transport cause, which a bare to_string() drops. Multi-line anyhow chains are fine for the log/observability sink; nothing downstream parses it as a single line.
  • RPC handlers hardcode source = "user" (M3gA-Mind, cosmetic nitpick) — declining for this slice: every stop/resume caller here is the UI RPC path, so "user" is accurate. Threading a real "hotkey"|"system" source belongs with the future CLI/hotkey/system callers that will actually produce those values; wiring it now would add an unused parameter. Left as-is.

Requesting a CodeRabbit re-review below to clear the stale CHANGES_REQUESTED.

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

@coderabbitai review — the audit-trail Major is reconciled per your second option (spec Task 7 now carries the explicit deferred-audit note, so spec ↔ code are consistent); the panic-safe test guard is added; and the halt-broadcast/registration/cron/resume items are already fixed in the current head (details above).

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -uo pipefail
echo "=== spec Task 7 audit-trail note ==="
fd "2026-07-06-emergency-stop-design.md" docs --exec grep -n -i "audit" -A5 -B5 {}
echo "=== plan line ~745 ==="
fd "2026-07-06-emergency-stop.md" docs --exec sed -n '720,760p' {}
echo "=== ops.rs guard usage ==="
fd ops.rs src/openhuman/emergency_stop --exec cat -n {}
echo "=== socketService.ts halt handler ==="
fd socketService.ts app/src/services --exec sed -n '440,510p' {}
echo "=== startup.rs registration ==="
fd startup.rs src/openhuman/channels/runtime --exec cat -n {}
echo "=== jsonrpc.rs registration ==="
rg -n "register_automation_halt_subscriber" src/core/jsonrpc.rs
echo "=== cron scheduler halt check ==="
rg -n "is_engaged_global" src/openhuman/cron/scheduler.rs
echo "=== AutomationHaltedBanner.tsx resume onClick ==="
fd AutomationHaltedBanner.tsx app/src/components/safety --exec cat -n {}

Length of output: 87478


@M3gA-Mind Verified all the items against current HEAD:

  • Audit-trail reconciliation: docs/superpowers/specs/2026-07-06-emergency-stop-design.md (Line 47) now carries the explicit scope note ("the refusal is surfaced via a tracing::warn! and the AutomationHalted domain event / automation_halt socket broadcast, but is not recorded through ApprovalGate::intercept_audited... tracked as a follow-up"), and the plan doc's Task 7 has the matching "Audit-trail note (deferred)" callout right after the wrap_tool snippet. Spec and code are consistent now.
  • Panic-safe test guard: all three tests in src/openhuman/emergency_stop/ops.rs (stop_sets_flag_and_status_reports_engaged, resume_clears_flag, stop_is_idempotent) now acquire EMERGENCY_TEST_GUARD and immediately bind let _reset = ClearEmergencyOnDrop;, so a mid-test panic can't leak engaged state into later tests.
  • Halt-broadcast payload nesting: socketService.ts's automation_halt handler reads halt fields from obj.args (falling back to top-level) and drops any payload where engaged isn't a strict boolean.
  • Registration on desktop (web-chat-only): register_automation_halt_subscriber() is now registered both in start_channels (startup.rs Line 178) and on the always-run boot path (core/jsonrpc.rs Line 2351), and the registration is OnceLock-guarded/idempotent.
  • Cron fail-closed check: src/openhuman/cron/scheduler.rs Line 495 gates on is_engaged_global() at the outer dispatch level.
  • Resume-failure preserves halt: AutomationHaltedBanner.tsx's onResume only dispatches clearHalt() inside the try block after a confirmed emergencyResume(); the catch path sets resumeFailed without touching the halt state.

Everything checks out. Kicking off a fresh review pass against current HEAD.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Coding-Dev-Tools

Copy link
Copy Markdown

🤖 Council Merge Gate — APPROVE (merge blocked by branch protection)

The model council review gate returned APPROVE (session council-6ee6b252-706a-4426-bbd7-e8aa0a4e984a), so the change is council-cleared.

However, gh pr merge was rejected by GitHub branch protection: the base branch policy prohibits the merge. This repo requires passing status checks and/or an approving review before merge, and those requirements are not currently satisfied (e.g. failing required CI checks).

Action needed (human): resolve the failing required checks / obtain the required review, then merge. A maintainer with admin rights may also merge explicitly. The PR is left OPEN.

Posted automatically by the council_gate_merge cron job.

@Coding-Dev-Tools

Copy link
Copy Markdown

🤖 Council Merge Gate — APPROVE (merge blocked by branch protection)

The model council review gate returned APPROVE (session council-1b5f62f7-0c43-41b5-8965-dfc8f3c921db), so the change is council-cleared.

However, gh pr merge was rejected by GitHub branch protection: the base branch policy prohibits the merge. This repo requires passing status checks and/or an approving review before merge, and those requirements are not currently satisfied (e.g. failing required CI checks).

Action needed (human): resolve the failing required checks / obtain the required review, then merge. A maintainer with admin rights may also merge explicitly. The PR is left OPEN.

Posted automatically by the council_gate_merge cron job.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cc56bf59a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/openhuman/cron/scheduler.rs
Comment thread app/src/App.tsx
Comment thread app/src/lib/i18n/en.ts Outdated
# Conflicts:
#	app/src/lib/i18n/en.ts
The locale coverage test (app/src/lib/i18n/__tests__/coverage.test.ts)
forbids U+2014 in every locale value, including en. The emergency-stop
safety.stopFailed / safety.resumeFailed strings used em dashes across 12
locale files; rephrase each into natural two-sentence copy with
locale-appropriate punctuation. No key or meaning changes.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Maintainer sync + conflict resolution (this PR had gone stale for ~10 days; main moved a lot underneath it).

Merge: merged current main into the PR branch. Exactly one textual conflict:

  • app/src/lib/i18n/en.ts — a pure additive collision at the end of the translations object. This PR appends the safety.* emergency-stop keys; main (via the coding-sessions memory-source feature) appended memorySources.codingSessions.* keys at the same anchor. Both key sets are disjoint, so I kept both. Verified every locale still carries both key families (all 14 locales) and tsc --noEmit is clean on the merged tree. All other files (including src/openhuman/tinyagents/middleware.rs, tests/json_rpc_e2e.rs, and the other 13 locale files) auto-merged.

Semantic re-check of the auto-merges (a clean textual merge can still be semantically broken): I verified the three emergency-stop enforcement chokepoints are still correctly wired after main's drift:

  • tinyagents/middleware.rs wrap_tool — the emergency_halt_denial short-circuit still fires on the external-effect path before the approval gate (fail-closed). main's changes here were in the unrelated circuit-breaker/halt-summary code and did not collide.
  • screen_intelligence/ops.rs — desktop input still blocked while halted, with panic_stop correctly exempt.
  • cron/scheduler.rs — scheduled jobs still refused while halted, fail-closed.

Genuine failure fixed (NOT inherited from the old red main): the Frontend Checks failure was the locale coverage test app/src/lib/i18n/__tests__/coverage.test.tslocale %s contains no em dashes, which forbids U+2014 in every locale value (including en). The safety.stopFailed / safety.resumeFailed strings used em dashes in 12 locale files. I rephrased each into natural two-sentence copy with locale-appropriate punctuation (period for most; full-width comma for zh-CN), preserving meaning and keys. de/es already used an en-dash/colon and were untouched. Verified 0 remaining U+2014 in any locale value; ran Prettier on the touched files (it collapsed the now-shorter ko/pl resumeFailed onto one line).

Inherited #4948 breakage: gone — the sync pulled in main's #4949 fix, and the merged-tree typecheck is clean.

Pushed 2 commits to the fork head: the merge commit + fix(i18n): remove em dashes from safety.* translations. Re-review of the feature itself follows in a separate comment.

…above webviews

cron: the emergency-stop guard ran once before the retry loop, so a user
who engaged Emergency Stop during a backoff sleep could still have the
next retry execute a Shell/Flow/Agent job. Re-check is_engaged_global()
at the top of each retry attempt and return the same fail-closed denial
(Codex P1, tinyhumansai#4255).

UI: AutomationHaltedBanner rendered in normal flow, so the provider
WebviewHost overlay (absolute inset-0 z-30) covered the halt banner and
its Resume button whenever a provider account was active. Make the banner
sticky top-0 z-40 so it stays visible and reachable above the webview,
below the settings modal (z-50), per the app stacking convention (Codex P2).
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Maintainer re-review (PR was untouched for ~10 days; reviewing it current, and clearing the outstanding bot items).

Re-review of the feature — looks solid

The emergency-stop design is well-built and genuinely fail-closed:

  • emergency_stop/state.rs — process-global OnceLock switch mirroring ApprovalGate. try_global() returns None on CLI/headless so those paths are never blocked; engage/clear write the flag under the info lock so snapshot() never sees a torn (flag, info) pair, while the lock-free is_engaged() fast path is exactly what the enforcement chokepoints need. Idempotent throughout.
  • Three enforcement chokepoints, all fail-closed, and all verified still correctly wired after the merge with main: tinyagents/middleware.rs::wrap_tool (denies external-effect tools before the approval gate), screen_intelligence/ops.rs (blocks desktop input, correctly exempts panic_stop), and cron/scheduler.rs (blocks Shell/Flow/Agent jobs — the two that bypass the tool middleware).

Fixes I pushed

  1. [Codex P1] Re-check the kill switch on every cron retry (cron/scheduler.rs) — the guard ran once before for attempt in 0..=retries, so engaging Emergency Stop during a backoff sleep still let the next retry execute a Shell/Flow/Agent job. Now re-checks is_engaged_global() at the top of each retry attempt and returns the same fail-closed denial. Confirmed real; fixed.
  2. [Codex P2] Halt banner hidden behind provider webviews (AutomationHaltedBanner.tsx) — the banner rendered in normal flow while the provider WebviewHost is an absolute inset-0 z-30 sibling that covered it, so with a provider account active the halt banner + Resume button were occluded. Made the banner sticky top-0 z-40 (above the z-30 webview, below the z-50 settings modal, per the app's documented stacking convention). Confirmed real; fixed. ⚠️ Please give this one a quick visual confirmation on the desktop (CEF) build — I can't run the desktop app in this environment, and CEF overlay stacking is worth an eyeball.
  3. [Codex P2] Em dashes in safety translations — fixed as part of the sync (see the sync comment); the locale coverage test forbids U+2014 in every locale value.

Bot items already addressed by the author (verified against current code)

  • [CodeRabbit Major] Stale resumeFailed across halt cycles — handled: useEffect(() => { if (!halted) setResumeFailed(false) }, [halted]) resets it each cycle.
  • [CodeRabbit Minor] Empty-string reason bypasses fallback — handled: the banner uses reason || t('safety.haltedBody') (||, so '' falls back), not the ?? the comment flagged.
  • [CodeRabbit Major] e2e leaks halted state on panic — handled: json_rpc_emergency_stop_roundtrip_over_rpc installs a ResumeOnDrop RAII guard that clears the global switch on unwind.
  • [Codex P1] Scheduled jobs unchecked — handled: the cron/scheduler.rs outermost dispatch guard covers Shell/Flow/Agent.
  • [Codex P2] Preserve halt state when resume fails — handled: the banner only clearHalt()s on a confirmed resume and sets resumeFailed on error.

Recommendation

Merge once CI is green, pending a quick visual check of the halt-banner z-index on desktop (item 2). No open correctness concerns from me; the safety enforcement is fail-closed at every path I traced.

@M3gA-Mind M3gA-Mind left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. CI is fully green (18 lanes: 14 pass, 4 skipped by changed-area detection, zero pending, zero failing) — including Frontend Checks, PR CI Gate, Rust Quality (fmt/clippy), Rust Feature-Gate Smoke (gates off), and Rust Core Coverage. mergeable=MERGEABLE; no standing CHANGES_REQUESTED (CodeRabbit's was dismissed).

This PR sat at review-complete for ~10 days and had drifted. Bringing it current surfaced real issues, all now fixed:

Conflict with main — resolved (understood both sides, no blanket --ours/--theirs).

Frontend Checks was a genuine failure, not inherited. Worth stating plainly, since main was separately red today (#4948/#4949): this PR's failure was its own. safety.stopFailed / safety.resumeFailed used em dashes (U+2014) across 12 locale files, which the i18n rule forbids in every locale including en. Each was rephrased into natural two-sentence copy with locale-appropriate punctuation (period; full-width comma for zh-CN), preserving meaning and keys. de/es already used en-dash/colon and were left alone. Zero U+2014 remain. PR CI Gate was only a rollup of that failure and cleared with it.

Two Codex findings fixed — one is a real safety defect:

  1. [P1] Emergency Stop could be bypassed during retry backoff. The kill-switch guard ran once before the retry loop, so engaging the stop during a backoff sleep let the next attempt execute a Shell/Flow/Agent job anyway. Now re-checked before every attempt, fail-closed. In a feature whose entire purpose is to halt execution, this mattered.
  2. [P2] Halt banner was rendering behind provider webviews — it sat in normal flow under the absolute inset-0 z-30 WebviewHost. Now sticky top-0 z-40.

Bot items: every CodeRabbit and Codex item is fixed or verified already-addressed by the author (stale-resumeFailed useEffect reset, empty-string reason fallback, e2e panic cleanup via ResumeOnDrop RAII, cron dispatch guard, preserve-halt-on-failed-resume). None left unaddressed.

Assessment: the state machine is fail-closed, atomic, and headless-safe; enforcement covers the middleware plus both non-middleware job paths and desktop input, with panic_stop correctly exempt. No open correctness concerns.

One caveat the maintainer should know before merging: the P2 banner z-index fix is not visually verified — it was made and reasoned about headless, and no one has confirmed the banner actually renders above the CEF webview in the running desktop app. The logic is sound and the risk is low, but this is a safety-critical affordance: if the halt banner is invisible, the user cannot tell the system is stopped. Recommend a quick desktop look at that banner before or shortly after merge.

Note for the merger: the head (e340a8be8) carries maintainer commits pushed unsigned (51da322, e5bcf46, e340a8b) — re-sign before merging if signature enforcement applies.

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

@coderabbitai review

Requesting a re-review of the current head (e340a8be8). Your earlier review on this PR was dismissed and the tree has moved substantially since: the branch was re-synced onto current main, the merge conflict resolved, a genuine i18n em-dash violation fixed across 12 locale files, and two Codex findings addressed (retry-loop kill-switch re-check; halt-banner z-index). All 18 CI lanes are now green.

@M3gA-Mind
M3gA-Mind merged commit f145735 into tinyhumansai:main Jul 16, 2026
18 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Team Openhuman Jul 16, 2026
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== PR metadata ==="
gh pr view 4600 --repo tinyhumansai/openhuman --json headRefOid,baseRefName,headRefName,mergeable,state,statusCheckRollup \
  --jq '{headRefOid, baseRefName, headRefName, mergeable, state, checks: [.statusCheckRollup[] | {name, conclusion, status}]}'

echo "=== Changed files (merge-base diff) ==="
git diff --name-status origin/main...HEAD

echo "=== Diff stat ==="
git diff --stat origin/main...HEAD

echo "=== Targeted AST outlines ==="
ast-grep outline src/openhuman/cron/scheduler.rs --items all --type function --match 'execute_job_with_retry|is_engaged_global' || true
ast-grep outline app/src/components/safety/AutomationHaltedBanner.tsx --items all || true
ast-grep outline app/src/services/socketService.ts --items all --match 'connect|socket' || true
ast-grep outline src/openhuman/emergency_stop --items all || true

Length of output: 9912


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Current focused implementations ==="
sed -n '1,240p' src/openhuman/emergency_stop/state.rs
sed -n '1,260p' src/openhuman/emergency_stop/ops.rs
sed -n '450,610p' src/openhuman/cron/scheduler.rs
sed -n '1,150p' app/src/components/safety/AutomationHaltedBanner.tsx
sed -n '430,530p' app/src/services/socketService.ts

echo "=== Controller/event registration and bridge ==="
sed -n '1,220p' src/openhuman/emergency_stop/schemas.rs
rg -n -C 4 'emergency_stop|register_automation_halt_subscriber' src/core/all.rs src/core/jsonrpc.rs src/openhuman/channels/runtime/startup.rs src/openhuman/channels/providers/web/mod.rs
ast-grep outline src/openhuman/channels/providers/web/event_bus.rs --items all --match 'AutomationHalt|register_automation_halt_subscriber'
rg -n -C 5 'AutomationHaltSubscriber|automation_halt|register_automation_halt_subscriber' src/openhuman/channels/providers/web/event_bus.rs

echo "=== Enforcement guards ==="
rg -n -C 10 'emergency_halt_denial|is_engaged_global|panic_stop' src/openhuman/tinyagents/middleware.rs src/openhuman/screen_intelligence/ops.rs

echo "=== Frontend API, state, boot integration ==="
cat -n app/src/services/api/emergencyApi.ts
cat -n app/src/store/safetySlice.ts
rg -n -C 6 'hydrateEmergencyState|AutomationHaltedBanner|EmergencyStopButton' app/src/App.tsx

echo "=== Recent-change patches ==="
git diff origin/main...HEAD -- src/openhuman/cron/scheduler.rs app/src/components/safety/AutomationHaltedBanner.tsx

Length of output: 74842


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Cron command execution and retry/backoff boundaries ==="
rg -n -C 12 'async fn run_job_command|Command::new|for attempt in|sleep\(|backoff_ms|execute_job_with_retry' src/openhuman/cron/scheduler.rs

echo "=== Emergency-stop cron regression tests ==="
rg -n -C 12 'emergency|halted|retry' src/openhuman/cron/scheduler_tests.rs

echo "=== UI/API/state regression tests ==="
sed -n '1,180p' app/src/components/safety/AutomationHaltedBanner.test.tsx
sed -n '1,140p' app/src/components/safety/EmergencyStopButton.test.tsx
sed -n '1,150p' app/src/services/__tests__/socketService.events.test.ts
sed -n '1,120p' app/src/services/safety/hydrateEmergencyState.test.ts
sed -n '1,120p' app/src/services/api/emergencyApi.test.ts

echo "=== Runtime installation branch and complete core diff context ==="
sed -n '2310,2395p' src/core/jsonrpc.rs
git diff --check origin/main...HEAD
git diff --unified=12 origin/main...HEAD -- src/core/jsonrpc.rs src/openhuman/screen_intelligence/ops.rs src/openhuman/tinyagents/middleware.rs tests/json_rpc_e2e.rs

Length of output: 79535


@M3gA-Mind I completed the re-review of e340a8be8. The retry-loop guard, halt-banner stacking fix, socket payload handling, state synchronization, and the Rust/frontend regression coverage all look correct. CI is also green.

One remaining correctness concern:

  • Running scheduled shell jobs are not stopped after Emergency Stop is engaged. execute_job_with_retry correctly refuses work before attempt 0 and before each retry, but once run_job_command_with_timeout starts its sh -lc child at Line 1464, emergency_stop() has no handle or cancellation registry through which to terminate it. Thus, a shell job that begins immediately before the stop RPC can continue executing and producing external effects until it exits or times out. The current cron test covers only a halt that exists before launch.

    This conflicts with the stated “halts running and queued automation” contract. Please either wire active cron child processes into emergency-stop cancellation (and add a race regression test: start a long-running job → engage stop → assert termination), or explicitly narrow this slice’s contract to blocking only future scheduled-job attempts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Desktop automation safety previews, confirmations, history, and emergency stop

6 participants