feat(background): runner PTY B8 + boucle sink fermée + refactor point-2
Livre le lot backend B8 des tâches de fond : - infrastructure : runner de commandes concret (CommandBackgroundRunner) sur le port BackgroundTaskRunner, tail borné (bounded_tail/BoundedTail), éclatement du module background_task en sous-modules (mod/runner/tail, sink extrait de l'ancien background_task.rs). - application : nouveau module background exposant les cas d'usage SpawnBackgroundCommand, CancelBackgroundTask, RetryBackgroundTask et le port BackgroundCommandArchive. - domain : refactor point-2 de l'arbitrage Architect — sortie du trait BackgroundCommandArchive de la couche domaine vers application. - app-tauri : câblage runtime (commands, dto, state, lib) des commandes spawn/cancel/retry et de la boucle de complétion sink fermée en composition root. Build workspace + tests application/infrastructure verts (QA). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -30,7 +30,8 @@ use domain::ports::PtyHandle;
|
||||
use crate::dto::{
|
||||
parse_agent_id, parse_close_terminal, parse_delete_profile, parse_layout_id, parse_memory_slug,
|
||||
parse_node_id, parse_profile_id, parse_project_id, parse_session_id, parse_skill_id,
|
||||
parse_template_id, parse_ticket_id, AgentDriftListDto, AgentDto, AgentListDto,
|
||||
parse_task_id, parse_template_id, parse_ticket_id, AgentDriftListDto, AgentDto, AgentListDto,
|
||||
BackgroundTaskDto,
|
||||
AssignSkillRequestDto, AttachLiveAgentRequestDto, AttachLiveAgentResponseDto,
|
||||
ChangeAgentProfileDto, ChangeAgentProfileRequestDto, ConfigureProfilesRequestDto,
|
||||
ConversationDetailsDto, CreateAgentFromTemplateRequestDto, CreateAgentRequestDto,
|
||||
@ -2555,3 +2556,160 @@ pub async fn resolve_memory_links(
|
||||
.map(MemoryLinksDto::from)
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background tasks (B8 — first-class background command)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Maps a background-task port error to the frontend [`ErrorDto`] shape.
|
||||
fn background_error(err: domain::ports::BackgroundTaskPortError) -> ErrorDto {
|
||||
use domain::ports::BackgroundTaskPortError as E;
|
||||
let code = match &err {
|
||||
E::NotFound => "NOT_FOUND",
|
||||
E::AlreadyExists | E::Invalid(_) => "INVALID",
|
||||
E::Runner(_) => "PROCESS",
|
||||
E::Store(_) => "STORE",
|
||||
};
|
||||
ErrorDto {
|
||||
code: code.to_owned(),
|
||||
message: err.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `spawn_background_command` — start a command-backed first-class background
|
||||
/// task whose completion is delivered to the owning agent's inbox.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id/path, `PROCESS` if the
|
||||
/// command cannot be spawned, `STORE` on persistence failure).
|
||||
#[tauri::command]
|
||||
pub async fn spawn_background_command(
|
||||
request: crate::dto::SpawnBackgroundCommandRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<BackgroundTaskDto, ErrorDto> {
|
||||
let project_id = parse_project_id(&request.project_id)?;
|
||||
let owner_agent_id = parse_agent_id(&request.owner_agent_id)?;
|
||||
let cwd = domain::project::ProjectPath::new(request.cwd.clone()).map_err(|_| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("invalid working directory: {}", request.cwd),
|
||||
})?;
|
||||
let command = domain::ports::SpawnSpec {
|
||||
command: request.command,
|
||||
args: request.args,
|
||||
cwd,
|
||||
env: request.env,
|
||||
context_plan: None,
|
||||
sandbox: None,
|
||||
};
|
||||
let wake_policy = if request.record_only {
|
||||
domain::BackgroundTaskWakePolicy::RecordOnly
|
||||
} else {
|
||||
domain::BackgroundTaskWakePolicy::WakeOwner
|
||||
};
|
||||
state
|
||||
.spawn_background_command
|
||||
.execute(application::SpawnBackgroundCommandInput {
|
||||
project_id,
|
||||
owner_agent_id,
|
||||
label: request.label,
|
||||
command,
|
||||
wake_policy,
|
||||
deadline_ms: request.deadline_ms,
|
||||
})
|
||||
.await
|
||||
.map(|out| BackgroundTaskDto::from(out.task))
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `cancel_background_task` — request cancellation of a running background task.
|
||||
///
|
||||
/// The terminal `Cancelled` state is written by the completion sink; this returns
|
||||
/// the task as currently persisted (absent if it no longer exists).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `PROCESS`/`STORE` on
|
||||
/// failure).
|
||||
#[tauri::command]
|
||||
pub async fn cancel_background_task(
|
||||
task_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Option<BackgroundTaskDto>, ErrorDto> {
|
||||
let id = parse_task_id(&task_id)?;
|
||||
state
|
||||
.cancel_background_task
|
||||
.execute(id)
|
||||
.await
|
||||
.map(|out| out.task.map(BackgroundTaskDto::from))
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `retry_background_task` — re-run a terminal command task under a **new** task
|
||||
/// id (the original id is never reused).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id or a non-retryable task,
|
||||
/// `NOT_FOUND` if the task is unknown, `PROCESS`/`STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn retry_background_task(
|
||||
task_id: String,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<BackgroundTaskDto, ErrorDto> {
|
||||
let id = parse_task_id(&task_id)?;
|
||||
state
|
||||
.retry_background_task
|
||||
.execute(id)
|
||||
.await
|
||||
.map(|out| BackgroundTaskDto::from(out.task))
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
/// `list_background_tasks` — read the background-task read-model for a project,
|
||||
/// optionally narrowed to one owning agent.
|
||||
///
|
||||
/// Returns the union of the agent's open tasks and undelivered completions,
|
||||
/// filtered to `projectId`. Without `agentId`, only the project's undelivered
|
||||
/// completions are returned (the store cannot enumerate open tasks project-wide).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `STORE` on failure).
|
||||
#[tauri::command]
|
||||
pub async fn list_background_tasks(
|
||||
project_id: String,
|
||||
agent_id: Option<String>,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<Vec<BackgroundTaskDto>, ErrorDto> {
|
||||
let project_id = parse_project_id(&project_id)?;
|
||||
let agent_id = match &agent_id {
|
||||
Some(raw) => Some(parse_agent_id(raw)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let store = &state.background_task_store;
|
||||
let mut by_id: std::collections::HashMap<domain::TaskId, domain::BackgroundTask> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
if let Some(agent_id) = agent_id {
|
||||
for task in store
|
||||
.list_open_for_agent(agent_id)
|
||||
.await
|
||||
.map_err(background_error)?
|
||||
{
|
||||
by_id.insert(task.id, task);
|
||||
}
|
||||
}
|
||||
for task in store
|
||||
.list_undelivered_completions()
|
||||
.await
|
||||
.map_err(background_error)?
|
||||
{
|
||||
by_id.entry(task.id).or_insert(task);
|
||||
}
|
||||
|
||||
let mut tasks: Vec<domain::BackgroundTask> = by_id
|
||||
.into_values()
|
||||
.filter(|task| task.project_id == project_id)
|
||||
.filter(|task| agent_id.map_or(true, |a| task.owner_agent_id == a))
|
||||
.collect();
|
||||
tasks.sort_by_key(|task| (task.created_at_ms, task.id));
|
||||
Ok(tasks.into_iter().map(BackgroundTaskDto::from).collect())
|
||||
}
|
||||
|
||||
@ -2866,3 +2866,167 @@ mod ls6_pagination_tests {
|
||||
assert!(dto.turns.is_empty() && !dto.has_more && dto.next_anchor.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Background tasks (B8 — first-class background command)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use domain::{
|
||||
BackgroundTask, BackgroundTaskKind, BackgroundTaskResult, BackgroundTaskState, TaskId,
|
||||
};
|
||||
|
||||
/// One first-class background task as seen by the frontend (camelCase wire shape).
|
||||
///
|
||||
/// Mirrors the persisted [`BackgroundTask`], flattening the terminal
|
||||
/// [`BackgroundTaskResult`] into optional `exitCode` / `summary` / tails so the
|
||||
/// UI has a single shape for every lifecycle state. Optional fields are omitted
|
||||
/// from the wire when absent (`skip_serializing_if`).
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BackgroundTaskDto {
|
||||
/// Stable task id (UUID string).
|
||||
pub task_id: String,
|
||||
/// Owning agent id (UUID string).
|
||||
pub owner_agent_id: String,
|
||||
/// Owning project id (UUID string).
|
||||
pub project_id: String,
|
||||
/// Kind discriminant (`"command"`, `"headlessRendezvous"`, `"sessionResume"`,
|
||||
/// `"maintenance"`).
|
||||
pub kind: String,
|
||||
/// Lifecycle state (`"queued"`, `"running"`, `"waiting"`, `"completed"`,
|
||||
/// `"failed"`, `"cancelled"`, `"expired"`).
|
||||
pub state: String,
|
||||
/// Process exit code, when the terminal result carries one.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub exit_code: Option<i32>,
|
||||
/// Human-readable summary / error / reason of the terminal result.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub summary: Option<String>,
|
||||
/// Bounded stdout tail (merged output for PTY-backed commands).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stdout_tail: Option<String>,
|
||||
/// Bounded stderr tail (unset for PTY-backed commands, which merge streams).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stderr_tail: Option<String>,
|
||||
/// Creation timestamp, epoch milliseconds.
|
||||
pub created_at_ms: u64,
|
||||
/// Last update timestamp, epoch milliseconds.
|
||||
pub updated_at_ms: u64,
|
||||
}
|
||||
|
||||
/// Kind discriminant string for a [`BackgroundTaskKind`].
|
||||
fn background_kind_label(kind: &BackgroundTaskKind) -> &'static str {
|
||||
match kind {
|
||||
BackgroundTaskKind::Command { .. } => "command",
|
||||
BackgroundTaskKind::HeadlessRendezvous { .. } => "headlessRendezvous",
|
||||
BackgroundTaskKind::SessionResume { .. } => "sessionResume",
|
||||
BackgroundTaskKind::Maintenance { .. } => "maintenance",
|
||||
}
|
||||
}
|
||||
|
||||
/// Lifecycle state string for a [`BackgroundTaskState`].
|
||||
fn background_state_label(state: BackgroundTaskState) -> &'static str {
|
||||
match state {
|
||||
BackgroundTaskState::Queued => "queued",
|
||||
BackgroundTaskState::Running => "running",
|
||||
BackgroundTaskState::Waiting => "waiting",
|
||||
BackgroundTaskState::Completed => "completed",
|
||||
BackgroundTaskState::Failed => "failed",
|
||||
BackgroundTaskState::Cancelled => "cancelled",
|
||||
BackgroundTaskState::Expired => "expired",
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BackgroundTask> for BackgroundTaskDto {
|
||||
fn from(task: BackgroundTask) -> Self {
|
||||
let (exit_code, summary, stdout_tail, stderr_tail) = match &task.result {
|
||||
Some(BackgroundTaskResult::Success {
|
||||
exit_code,
|
||||
summary,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
..
|
||||
}) => (
|
||||
*exit_code,
|
||||
Some(summary.clone()),
|
||||
stdout_tail.clone(),
|
||||
stderr_tail.clone(),
|
||||
),
|
||||
Some(BackgroundTaskResult::Failure {
|
||||
exit_code,
|
||||
error,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
..
|
||||
}) => (
|
||||
*exit_code,
|
||||
Some(error.clone()),
|
||||
stdout_tail.clone(),
|
||||
stderr_tail.clone(),
|
||||
),
|
||||
Some(BackgroundTaskResult::Cancelled { reason, .. })
|
||||
| Some(BackgroundTaskResult::Expired { reason, .. }) => {
|
||||
(None, Some(reason.clone()), None, None)
|
||||
}
|
||||
None => (None, None, None, None),
|
||||
};
|
||||
Self {
|
||||
task_id: task.id.to_string(),
|
||||
owner_agent_id: task.owner_agent_id.to_string(),
|
||||
project_id: task.project_id.to_string(),
|
||||
kind: background_kind_label(&task.kind).to_owned(),
|
||||
state: background_state_label(task.state).to_owned(),
|
||||
exit_code,
|
||||
summary,
|
||||
stdout_tail,
|
||||
stderr_tail,
|
||||
created_at_ms: task.created_at_ms,
|
||||
updated_at_ms: task.updated_at_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a task-id string (UUID) coming from the frontend.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`ErrorDto`] with code `INVALID` if the string is not a UUID.
|
||||
pub fn parse_task_id(raw: &str) -> Result<TaskId, ErrorDto> {
|
||||
uuid::Uuid::parse_str(raw)
|
||||
.map(TaskId::from_uuid)
|
||||
.map_err(|_| ErrorDto {
|
||||
code: "INVALID".to_owned(),
|
||||
message: format!("invalid task id: {raw}"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Request DTO for `spawn_background_command`.
|
||||
///
|
||||
/// Builds a command-backed [`BackgroundTask`]: the command line runs under the
|
||||
/// project host's PTY and its completion is delivered to `ownerAgentId`.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SpawnBackgroundCommandRequestDto {
|
||||
/// Owning project id (UUID string).
|
||||
pub project_id: String,
|
||||
/// Agent that owns completion delivery (UUID string).
|
||||
pub owner_agent_id: String,
|
||||
/// Human-facing label.
|
||||
pub label: String,
|
||||
/// Executable to run.
|
||||
pub command: String,
|
||||
/// Arguments.
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
/// Working directory (absolute path).
|
||||
pub cwd: String,
|
||||
/// Extra environment variables.
|
||||
#[serde(default)]
|
||||
pub env: Vec<(String, String)>,
|
||||
/// When `true` (the default), wake the owner on completion; otherwise only
|
||||
/// record it.
|
||||
#[serde(default)]
|
||||
pub record_only: bool,
|
||||
/// Optional absolute deadline, epoch milliseconds.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub deadline_ms: Option<u64>,
|
||||
}
|
||||
|
||||
@ -214,6 +214,10 @@ pub fn run() {
|
||||
commands::recall_memory,
|
||||
commands::resolve_memory_links,
|
||||
commands::move_tab_to_new_window,
|
||||
commands::spawn_background_command,
|
||||
commands::cancel_background_task,
|
||||
commands::retry_background_task,
|
||||
commands::list_background_tasks,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running IdeA Tauri application");
|
||||
|
||||
@ -13,7 +13,8 @@ use std::sync::{Arc, Mutex};
|
||||
|
||||
use application::{
|
||||
AgentResumer, AgentWakeService, AppError, AssignSkillToAgent, AttachLiveAgent,
|
||||
ChangeAgentProfile, CheckEmbedderSuggestion, CloseProject, CloseTab, CloseTerminal,
|
||||
BackgroundCommandArchive, CancelBackgroundTask, ChangeAgentProfile, CheckEmbedderSuggestion,
|
||||
CloseProject, CloseTab, CloseTerminal, RetryBackgroundTask, SpawnBackgroundCommand,
|
||||
ConfigureProfiles, ContextGuardUseCases, CreateAgentFromScratch, CreateAgentFromTemplate,
|
||||
CreateLayout, CreateMemory, CreateProject, CreateSkill, CreateTemplate, DeleteAgent,
|
||||
DeleteEmbedderProfile, DeleteLayout, DeleteMemory, DeleteProfile, DeleteSkill, DeleteTemplate,
|
||||
@ -39,7 +40,8 @@ use application::{
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, AgentSession, AgentSessionFactory, AgentWakePort,
|
||||
BackgroundTaskPortError, BackgroundTaskStore, Clock, Embedder, EmbedderEnvInspector,
|
||||
BackgroundTaskPortError, BackgroundTaskRunner, BackgroundTaskStore,
|
||||
Clock, Embedder, EmbedderEnvInspector,
|
||||
EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator,
|
||||
MemoryRecall, MemoryStore, PermissionStore, ProcessSpawner, ProfileStore, ProjectStore,
|
||||
PtyPort, ScheduledTask, Scheduler, SkillStore, TemplateStore, WakeError, WakeReason,
|
||||
@ -57,8 +59,9 @@ use serde_json::{json, Map, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
use infrastructure::{
|
||||
embedder_from_profile, AdaptiveMemoryRecall, BackgroundTaskReadyToDeliver,
|
||||
ClaudePermissionProjector, ClaudeTranscriptInspector, CliAgentRuntime,
|
||||
embedder_from_profile, AdaptiveMemoryRecall, BackgroundCompletionSink,
|
||||
BackgroundTaskReadyToDeliver, ClaudePermissionProjector, ClaudeTranscriptInspector,
|
||||
CliAgentRuntime, CommandBackgroundRunner,
|
||||
CodexPermissionProjector, EmbedderEnvProbe, FsBackgroundTaskStore, FsConversationLog,
|
||||
FsEmbedderProfileStore, FsEmbedderPromptStore, FsHandoffStore, FsLiveStateStore, FsMemoryStore,
|
||||
FsOrchestratorWatcher, FsPermissionStore, FsProfileStore, FsProjectStore,
|
||||
@ -795,6 +798,15 @@ pub struct AppState {
|
||||
// --- Windows (L10) ---
|
||||
/// Detach a tab into a new OS window (persists the workspace topology).
|
||||
pub move_tab: Arc<MoveTabToNewWindow>,
|
||||
// --- Background tasks (B8) ---
|
||||
/// Spawn a command-backed first-class background task.
|
||||
pub spawn_background_command: Arc<SpawnBackgroundCommand>,
|
||||
/// Cancel a running background task.
|
||||
pub cancel_background_task: Arc<CancelBackgroundTask>,
|
||||
/// Retry a terminal command task under a fresh task id.
|
||||
pub retry_background_task: Arc<RetryBackgroundTask>,
|
||||
/// Store handle used by `list_background_tasks` to read the task read-model.
|
||||
pub background_task_store: Arc<dyn BackgroundTaskStore>,
|
||||
// --- Templates & sync (L7) ---
|
||||
/// Create a template in the global store.
|
||||
pub create_template: Arc<CreateTemplate>,
|
||||
@ -1541,6 +1553,49 @@ impl AppState {
|
||||
let background_tasks_port = Arc::clone(&background_tasks) as Arc<dyn BackgroundTaskStore>;
|
||||
let (background_ready_tx, mut background_ready_rx) =
|
||||
tokio::sync::mpsc::unbounded_channel::<BackgroundTaskReadyToDeliver>();
|
||||
|
||||
// --- B8 : runner de commandes + fermeture de la boucle du sink ---
|
||||
// Le runner concret exécute les tâches command-backed sur le `PtyPort` composé
|
||||
// (local ici ; SSH/WSL via `RemoteHost` quand ils atterriront — Liskov). Il
|
||||
// émet UNE complétion par tâche ; le sink (single-writer) persiste l'état
|
||||
// terminal AVANT de signaler la livraison (persist-avant-signal), puis le pont
|
||||
// ready→inbox ci-dessous réveille l'agent propriétaire.
|
||||
let background_runner = Arc::new(CommandBackgroundRunner::new(
|
||||
Arc::clone(&pty_port),
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
));
|
||||
let background_runner_port =
|
||||
Arc::clone(&background_runner) as Arc<dyn BackgroundTaskRunner>;
|
||||
{
|
||||
let sink = Arc::new(BackgroundCompletionSink::new(
|
||||
Arc::clone(&background_tasks_port),
|
||||
background_ready_tx.clone(),
|
||||
));
|
||||
let runner = Arc::clone(&background_runner_port);
|
||||
// `start_from_runner` exige un runtime Tokio ambiant (`Handle::current`) :
|
||||
// on l'appelle donc DANS la tâche async. Le `JoinHandle` du drain est gardé
|
||||
// vivant en l'attendant (il ne se termine qu'à la fermeture du flux de
|
||||
// complétions), ce qui maintient sink + runner en vie pour la session.
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let drain = sink.start_from_runner(runner);
|
||||
let _ = drain.await;
|
||||
});
|
||||
}
|
||||
let spawn_background_command = Arc::new(SpawnBackgroundCommand::new(
|
||||
Arc::clone(&background_tasks_port),
|
||||
Arc::clone(&background_runner_port),
|
||||
Arc::clone(&clock) as Arc<dyn Clock>,
|
||||
Arc::clone(&ids) as Arc<dyn IdGenerator>,
|
||||
));
|
||||
let cancel_background_task = Arc::new(CancelBackgroundTask::new(
|
||||
Arc::clone(&background_tasks_port),
|
||||
Arc::clone(&background_runner_port),
|
||||
));
|
||||
let retry_background_task = Arc::new(RetryBackgroundTask::new(
|
||||
Arc::clone(&background_tasks_port),
|
||||
Arc::clone(&background_runner) as Arc<dyn BackgroundCommandArchive>,
|
||||
Arc::clone(&spawn_background_command),
|
||||
));
|
||||
let background_wake = Arc::new(AgentWakeService::new(
|
||||
Arc::clone(&mediated_inbox) as Arc<dyn AgentInbox>,
|
||||
Arc::clone(&input_mediator),
|
||||
@ -1977,6 +2032,10 @@ impl AppState {
|
||||
fs_port: Arc::clone(&fs_port),
|
||||
home_dir,
|
||||
move_tab,
|
||||
spawn_background_command,
|
||||
cancel_background_task,
|
||||
retry_background_task,
|
||||
background_task_store: Arc::clone(&background_tasks_port),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user