Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions src/core/event_bus/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,9 +563,35 @@ pub enum DomainEvent {
ApprovalDecided {
request_id: String,
tool_name: String,
/// `"approve_once"`, `"approve_always_for_tool"`, or `"deny"`.
/// `"approve_once"`, `"approve_always_for_tool"`,
/// `"approve_always_for_flow"`, or `"deny"`.
decision: String,
},
/// A `Workflow`-origin tool call parked in the `ApprovalGate` (issue
/// flow-approval-surface, PR2/PR3). Unlike `ApprovalRequested`, this
/// event carries no `thread_id`/`client_id` — a flow run has neither, so
/// the generic chat-routed socket bridge
/// (`channels::providers::web::event_bus::ApprovalSurfaceSubscriber`)
/// silently drops it (that gap was the original silent-deadlock bug).
/// Published by `ApprovalGate::intercept_audited` alongside the existing
/// `ApprovalRequested`, bridged by `core::socketio` directly to a
/// broadcast (not per-room) `flow_approval_request` Socket.IO event so
/// the Workflows UI can surface and resolve the park without polling.
FlowApprovalRequested {
/// Unique id used to correlate the decision back to the parked
/// future — pass to `approval_decide` unchanged.
request_id: String,
/// The `flows::Flow` id whose run parked this call.
flow_id: String,
/// The run's stable identifier (== the tinyflows checkpointer
/// thread id).
run_id: String,
/// Tool name being gated (e.g. `"composio"`).
tool_name: String,
/// Short human-readable summary of the action (redacted, same as
/// `ApprovalRequested::action_summary`).
summary: String,
},

// ── Plan review (interactive plan-mode gate) ────────────────────────
/// An interactive turn parked on a thread-scoped plan the user must
Expand Down Expand Up @@ -1412,7 +1438,8 @@ impl DomainEvent {
Self::ApprovalRequested { .. }
| Self::ApprovalDecided { .. }
| Self::ApprovalGateOverrideIgnored { .. }
| Self::ApprovalGateDisabled { .. } => "approval",
| Self::ApprovalGateDisabled { .. }
| Self::FlowApprovalRequested { .. } => "approval",

Self::PlanReviewRequested { .. } | Self::PlanReviewDecided { .. } => "plan_review",

Expand Down Expand Up @@ -1549,6 +1576,7 @@ impl DomainEvent {
Self::SessionExpired { .. } => "SessionExpired",
Self::ApprovalRequested { .. } => "ApprovalRequested",
Self::ApprovalDecided { .. } => "ApprovalDecided",
Self::FlowApprovalRequested { .. } => "FlowApprovalRequested",
Self::PlanReviewRequested { .. } => "PlanReviewRequested",
Self::PlanReviewDecided { .. } => "PlanReviewDecided",
Self::ApprovalGateOverrideIgnored { .. } => "ApprovalGateOverrideIgnored",
Expand Down
29 changes: 29 additions & 0 deletions src/core/socketio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,35 @@ pub fn spawn_web_channel_bridge(io: SocketIo) {
let _ = io_memory_sync.emit("flow:run_progress", &payload);
let _ = io_memory_sync.emit("flow_run_progress", &payload);
}
// A Workflow-origin tool call parked in the `ApprovalGate`
// (flow-approval-surface, PR2/PR3). Broadcast — not
// room-scoped like `ApprovalRequested`'s `approval_request`
// bridge — because a flow run has no chat thread/client to
// target; the Workflows UI listens process-wide and filters
// by `flow_id`/`run_id` client-side.
crate::core::event_bus::DomainEvent::FlowApprovalRequested {
request_id,
flow_id,
run_id,
tool_name,
summary,
} => {
let payload = serde_json::json!({
"request_id": request_id,
"flow_id": flow_id,
"run_id": run_id,
"tool_name": tool_name,
"summary": summary,
});
log::info!(
"[socketio] broadcast flow_approval_request request_id={} flow_id={} run_id={} tool={}",
request_id,
flow_id,
run_id,
tool_name
);
let _ = io_memory_sync.emit("flow_approval_request", &payload);
}
_ => {}
}
}
Expand Down
Loading
Loading