runtime: route notifications through worker inbox

This commit is contained in:
2026-08-05 01:44:57 +09:00
parent dd2ca54874
commit f98a123e40
15 changed files with 614 additions and 811 deletions
+23 -3
View File
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
#[serde(rename_all = "snake_case")]
pub enum WorkerInputKind {
User,
System,
Notify,
Compact,
ListRewindTargets,
RegisterPeer,
@@ -38,15 +38,35 @@ impl WorkerInput {
}
}
pub fn system(content: impl Into<String>) -> Self {
pub fn notify(content: impl Into<String>) -> Self {
Self {
kind: WorkerInputKind::System,
kind: WorkerInputKind::Notify,
content: content.into(),
segments: None,
}
}
}
#[cfg(test)]
mod tests {
use super::WorkerInput;
#[test]
fn notify_is_an_operation_and_legacy_system_kind_is_rejected() {
assert_eq!(
serde_json::to_value(WorkerInput::notify("message")).unwrap(),
serde_json::json!({ "kind": "notify", "content": "message" })
);
assert!(
serde_json::from_value::<WorkerInput>(serde_json::json!({
"kind": "system",
"content": "message"
}))
.is_err()
);
}
}
/// Acknowledgement returned after input is accepted into the Worker.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerInteractionAck {
+5 -5
View File
@@ -2392,9 +2392,9 @@ fn input_protocol_event(input: &WorkerInput) -> protocol::Event {
}]
}),
},
WorkerInputKind::System => protocol::Event::SystemItem {
WorkerInputKind::Notify => protocol::Event::SystemItem {
item: serde_json::json!({
"kind": "embedded_worker_system_input",
"kind": "embedded_worker_notification",
"content": input.content.clone(),
}),
},
@@ -3148,7 +3148,7 @@ mod tests {
fn create_worker_rejects_system_initial_input_without_persisting_worker() {
let runtime = runtime_with_backend();
let mut request = task_request("system initial input");
request.initial_input = Some(WorkerInput::system("role/system belongs in config bundle"));
request.initial_input = Some(WorkerInput::notify("role/system belongs in config bundle"));
let error = runtime.create_worker(request).unwrap_err();
assert!(matches!(
@@ -3390,7 +3390,7 @@ mod tests {
.send_input(&detail.worker_ref, WorkerInput::user("hello"))
.unwrap();
runtime
.send_input(&detail.worker_ref, WorkerInput::system("note"))
.send_input(&detail.worker_ref, WorkerInput::notify("note"))
.unwrap();
let observations = runtime
@@ -3550,7 +3550,7 @@ mod tests {
.send_input(&worker.worker_ref, WorkerInput::user("first"))
.unwrap();
runtime
.send_input(&worker.worker_ref, WorkerInput::system("second"))
.send_input(&worker.worker_ref, WorkerInput::notify("second"))
.unwrap();
runtime
.stop_worker(&worker.worker_ref, Some("finished".to_string()))
+76 -4
View File
@@ -793,6 +793,14 @@ fn method_starts_turn(method: &Method) -> bool {
)
}
fn accepted_notify_run_state(status: WorkerStatus, auto_run: bool) -> WorkerExecutionRunState {
match status {
WorkerStatus::Running => WorkerExecutionRunState::Busy,
WorkerStatus::Idle if auto_run => WorkerExecutionRunState::Busy,
WorkerStatus::Idle | WorkerStatus::Paused => WorkerExecutionRunState::Idle,
}
}
fn accepted_run_state_for_method(method: &Method) -> WorkerExecutionRunState {
match method {
Method::Run { .. }
@@ -1098,6 +1106,29 @@ where
}
};
if input.kind == WorkerInputKind::Notify {
let status = worker.shared_state.get_status();
let accepted_run_state = accepted_notify_run_state(status, true);
let claimed_here = status == WorkerStatus::Idle
&& busy
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok();
let result = self.send_method(
WorkerExecutionOperation::Input,
worker,
Method::Notify {
message: input.content,
auto_run: true,
},
accepted_run_state,
);
if claimed_here && result.outcome != crate::execution::WorkerExecutionOutcome::Accepted
{
busy.store(false, Ordering::SeqCst);
}
return result;
}
if worker.shared_state.get_status() != WorkerStatus::Idle
|| busy
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
@@ -1115,10 +1146,9 @@ where
.segments
.unwrap_or_else(|| vec![Segment::text(input.content.trim().to_string())]),
},
WorkerInputKind::System => Method::Notify {
message: input.content,
auto_run: true,
},
WorkerInputKind::Notify => {
unreachable!("Notify input is dispatched before the turn-start busy guard")
}
WorkerInputKind::Compact => Method::Compact,
WorkerInputKind::ListRewindTargets => Method::ListRewindTargets,
WorkerInputKind::RegisterPeer => Method::RegisterPeer {
@@ -1159,6 +1189,28 @@ where
}
};
if let Method::Notify { auto_run, .. } = &method {
let auto_run = *auto_run;
let status = worker.shared_state.get_status();
let accepted_run_state = accepted_notify_run_state(status, auto_run);
let claimed_here = status == WorkerStatus::Idle
&& auto_run
&& busy
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok();
let result = self.send_method(
WorkerExecutionOperation::ProtocolMethod,
worker,
method,
accepted_run_state,
);
if claimed_here && result.outcome != crate::execution::WorkerExecutionOutcome::Accepted
{
busy.store(false, Ordering::SeqCst);
}
return result;
}
let starts_turn = method_starts_turn(&method);
if starts_turn
&& (worker.shared_state.get_status() != WorkerStatus::Idle
@@ -1298,6 +1350,26 @@ mod tests {
use manifest::{Scope, WorkerManifest};
use session_store::WorkerMetadataStore;
#[test]
fn notify_run_state_allows_running_worker_inbox_delivery() {
assert_eq!(
accepted_notify_run_state(WorkerStatus::Running, true),
WorkerExecutionRunState::Busy
);
assert_eq!(
accepted_notify_run_state(WorkerStatus::Idle, true),
WorkerExecutionRunState::Busy
);
assert_eq!(
accepted_notify_run_state(WorkerStatus::Idle, false),
WorkerExecutionRunState::Idle
);
assert_eq!(
accepted_notify_run_state(WorkerStatus::Paused, true),
WorkerExecutionRunState::Idle
);
}
#[derive(Clone)]
struct MockClient {
responses: Arc<Vec<Vec<LlmEvent>>>,