Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
99a67ba
chore: record implementation plan
Copilot Oct 19, 2026
61266fb
fix(daemon): run project lifecycle on native runtime
Copilot Oct 19, 2026
f2db3d7
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 20, 2026
026ebed
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 20, 2026
d14005c
style(daemon): sort lifecycle hop imports
ScriptedAlchemy Oct 20, 2026
c06d2f6
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 20, 2026
11d36fe
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 21, 2026
fa70f0e
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 21, 2026
f7938b2
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 21, 2026
f07293a
simplify(runtime): remove obsolete lifecycle hop wrappers
ScriptedAlchemy Oct 21, 2026
ee130e4
fix(runtime): align shutdown callers with native deadlines
ScriptedAlchemy Oct 21, 2026
9dd6c41
fix(daemon): join warming opens before identity resolution
ScriptedAlchemy Oct 21, 2026
b2539ad
fix(runtime): await registered scope-set reads off native workers
ScriptedAlchemy Oct 21, 2026
8043da9
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 21, 2026
b344586
test(runtime): retain daemon context in native scope-set journey
ScriptedAlchemy Oct 21, 2026
d5075c1
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 21, 2026
bf19594
test(daemon): await tracked retirement before asserting receipt reaping
ScriptedAlchemy Oct 22, 2026
dd1b326
docs(daemon): keep runtime ownership comments concise
ScriptedAlchemy Oct 22, 2026
e1f6a54
Merge branch 'master' into copilot/finish-asupersync-runtime-cutover
ScriptedAlchemy Oct 22, 2026
1a667de
Merge remote-tracking branch 'origin/master' into copilot/finish-asup…
ScriptedAlchemy Oct 22, 2026
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
12 changes: 10 additions & 2 deletions crates/tracedecay-daemon-service/src/invocation/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,12 @@ impl DaemonInvocationService {
) -> Option<AuthorizedScopeSet> {
let owner = self.lsp_owner(Some(project_root)).await?;
let grant = owner.scope_grant?;
let scope_set = owner.scope_set_storage?.read(scope_set_id).ok().flatten()?;
let scope_set = owner
.scope_set_storage?
.read_async(scope_set_id)
.await
.ok()
.flatten()?;
// Persisted scope routes are actor-sealed: an admission on the active
// root authorizes reading only the scope sets its own actor persisted,
// never another actor's mounted routes.
Expand Down Expand Up @@ -221,7 +226,10 @@ impl DaemonInvocationService {
let active_owner = self.lsp_owner(Some(active_project_root)).await?;
let active_scope = active_owner.scope_grant.as_ref()?.scope.clone();
let active_storage = active_owner.scope_set_storage?;
let current = active_storage.read(&request.scope_set_id).ok()?;
let current = active_storage
.read_async(&request.scope_set_id)
.await
.ok()?;
let next_revision = match (request.expected_revision, current.as_ref()) {
(None, None) => ScopeSetRevision::new(1).ok()?,
(Some(expected), Some(current)) if current.revision() == expected => {
Expand Down
15 changes: 15 additions & 0 deletions crates/tracedecay-rusqlite-runtime/src/repository/scope_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,21 @@ impl AuthorizedScopeSetSqliteStorage {
decode_registered_rows(rows.rows)
}

/// Await reader admission and the query without parking a runtime worker.
pub async fn read_async(
&self,
scope_set_id: &ScopeSetId,
) -> Result<Option<AuthorizedScopeSet>, AuthorizedScopeSetStoreError> {
let rows = self
.handle()
.query_async(
registered_read_statement(scope_set_id)?,
std::time::Duration::from_secs(5),
)
.await?;
decode_registered_rows(rows.rows)
}

pub fn compare_and_swap(
&self,
expected_revision: Option<ScopeSetRevision>,
Expand Down
9 changes: 3 additions & 6 deletions crates/tracedecay/src/daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,6 @@ use project_open_handshake::{
};
mod project_open_orchestration;
mod project_routing;
// ambient-tokio: its Tokio timers run on Tokio tasks or inside the
// native_runtime::on_tokio hops in project_server_lifecycle_hops.
mod project_server_lifecycle;
#[cfg(unix)]
use project_open_orchestration::{
Expand Down Expand Up @@ -482,11 +480,10 @@ use project_routing::{
};
#[cfg(test)]
use project_server_lifecycle::replay_user_profile_host_admission_for_identity;
use project_server_lifecycle::{schedule_project_server_retirement, shutdown_project_servers};
mod project_server_lifecycle_hops;
use project_server_lifecycle_hops::{
use project_server_lifecycle::{
await_user_profile_host_admission_replay_for_identity, cancel_retained_session_history,
schedule_user_profile_host_admission_replay_for_identity,
schedule_project_server_retirement, schedule_user_profile_host_admission_replay_for_identity,
shutdown_project_servers,
};
#[cfg(unix)]
mod scheduler;
Expand Down
2 changes: 1 addition & 1 deletion crates/tracedecay/src/daemon/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ async fn run_foreground_loopback(
owner_phases,
move |project_server_deadline| async move {
shutdown_project_servers(
tokio::time::Instant::from_std(project_server_deadline),
project_server_deadline,
&server_store_administration,
&http_application_registry,
)
Expand Down
76 changes: 50 additions & 26 deletions crates/tracedecay/src/daemon/branch_admin/project_retirement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl ProjectServerRetirementAdmission<'_> {
track_project_server_retirement_after_admission(
&mut self.retirements,
owner,
spawn_tokio_retirement(retirement),
spawn_native_retirement(retirement),
);
}

Expand Down Expand Up @@ -144,31 +144,22 @@ where
};
finalizer.complete(status);
};
// Without a Tokio runtime the retirement is dropped unstarted, and its
// finalizer publishes the failed receipt.
if let Some(runtime) = native_runtime::tokio_handle(RETIREMENT_TOKIO_SITE) {
runtime.spawn(retirement);
if let Err(error) = NativeTask::spawn(retirement) {
tracing::error!(%error, "project server capacity retirement was refused");
}
completion
}

/// Retirements reach `project_server_lifecycle`, whose timers are Tokio's, so
/// they run on the daemon's Tokio runtime.
const RETIREMENT_TOKIO_SITE: &str = "daemon.project_retirement";

/// Spawns `retirement` on the Tokio runtime and returns its receipt. Without
/// a Tokio runtime the retirement never runs and its receipt is a failure.
fn spawn_tokio_retirement(
fn spawn_native_retirement(
retirement: impl std::future::Future<Output = ()> + Send + 'static,
) -> impl std::future::Future<Output = ProjectServerRetirementStatus> + Send + 'static {
let task = native_runtime::tokio_handle(RETIREMENT_TOKIO_SITE)
.map(|runtime| runtime.spawn(retirement));
let task = NativeTask::spawn(retirement);
async move {
match task {
Some(task) => tokio_retirement_status(task).await,
None => ProjectServerRetirementStatus::Failed(
"no Tokio runtime ran the project server retirement".to_owned(),
),
Ok(task) => native_retirement_status(task).await,
Err(error) => ProjectServerRetirementStatus::Failed(format!(
"native runtime refused project server retirement: {error}"
)),
}
}
}
Expand Down Expand Up @@ -226,6 +217,7 @@ async fn wait_for_project_server_retirement(
}
}

#[cfg(test)]
async fn tokio_retirement_status(
task: tokio::task::JoinHandle<()>,
) -> ProjectServerRetirementStatus {
Expand All @@ -248,6 +240,18 @@ async fn aborted_native_retirement_status(task: NativeTask<()>) -> ProjectServer
}
}

async fn native_retirement_status(task: NativeTask<()>) -> ProjectServerRetirementStatus {
match task.wait().await {
NativeTaskOutcome::Completed(()) => ProjectServerRetirementStatus::Clean,
NativeTaskOutcome::Cancelled => {
ProjectServerRetirementStatus::Failed("retirement task was cancelled".to_owned())
}
NativeTaskOutcome::Panicked(panic) => {
ProjectServerRetirementStatus::Failed(format!("task panicked: {panic}"))
}
}
}

/// Records one retirement and publishes `joined`'s status from a native
/// task. When the native runtime refuses that task, its finalizer publishes
/// a failed receipt.
Expand Down Expand Up @@ -293,13 +297,12 @@ pub(super) async fn track_retirement_task(
track_project_server_retirement(retirements, owner, tokio_retirement_status(task)).await;
}

/// Spawns `retirement` on the Tokio runtime and tracks its receipt.
pub(super) async fn track_tokio_retirement(
pub(super) async fn track_native_retirement(
retirements: &tokio::sync::Mutex<Vec<ProjectServerRetirement>>,
owner: StoreOwnerKey,
retirement: impl std::future::Future<Output = ()> + Send + 'static,
) {
track_project_server_retirement(retirements, owner, spawn_tokio_retirement(retirement)).await;
track_project_server_retirement(retirements, owner, spawn_native_retirement(retirement)).await;
}

pub(super) async fn track_aborted_native_retirement_task(
Expand Down Expand Up @@ -808,13 +811,19 @@ mod tests {
let capacity_admission = Arc::clone(&capacity_gate).lock_owned().await;
let release_capacity = Arc::new(tokio::sync::Notify::new());
let (started_tx, started_rx) = tokio::sync::oneshot::channel();
let ran_without_tokio = Arc::new(AtomicBool::new(false));
let mut admission = administration
.acquire_project_server_retirement_admission()
.await;
let completion = admission.spawn_and_track_fallible(owner("project-capacity"), {
let release_capacity = Arc::clone(&release_capacity);
let ran_without_tokio = Arc::clone(&ran_without_tokio);
async move {
let _capacity_admission = capacity_admission;
ran_without_tokio.store(
tokio::runtime::Handle::try_current().is_err(),
Ordering::Release,
);
let _ = started_tx.send(());
release_capacity.notified().await;
Ok(())
Expand All @@ -823,15 +832,30 @@ mod tests {
drop(admission);
drop(completion);
started_rx.await.expect("capacity retirement started");
assert!(
ran_without_tokio.load(Ordering::Acquire),
"capacity retirement must run on the native runtime without entering Tokio"
);
assert!(
Arc::clone(&capacity_gate).try_lock_owned().is_err(),
"a cancelled caller cannot release capacity while detached cleanup is pending"
);
let tracked_completion = administration
.acquire_project_server_retirement_admission()
.await
.prior_completions_for_owner(&owner("project-capacity"))
.pop()
.expect("the cancelled caller must leave a tracked capacity receipt");
release_capacity.notify_one();
let recovered_capacity = tokio::time::timeout(
std::time::Duration::from_secs(1),
Arc::clone(&capacity_gate).lock_owned(),
)
let recovered_capacity = tokio::time::timeout(std::time::Duration::from_secs(1), async {
let recovered_capacity = Arc::clone(&capacity_gate).lock_owned().await;
// The guard drops inside the retirement, before its terminal receipt is published.
tracked_completion
.wait()
.await
.expect("detached capacity retirement completes cleanly");
recovered_capacity
})
.await
.expect("detached cleanup must release capacity after completion");

Expand Down Expand Up @@ -1059,7 +1083,7 @@ mod tests {
let http_application_registry =
crate::daemon::http_application::DaemonHttpApplicationRegistry::default();
let mut shutdown = Box::pin(project_server_lifecycle::shutdown_project_servers(
tokio::time::Instant::now() + std::time::Duration::from_secs(5),
Instant::now() + std::time::Duration::from_secs(5),
&administration,
&http_application_registry,
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ pub(super) async fn retire_runtime_work(
}
super::super::project_server_lifecycle::retire_project_servers_now(servers).await;
};
super::project_retirement::track_tokio_retirement(tracked_retirements, owner, retirement)
super::project_retirement::track_native_retirement(tracked_retirements, owner, retirement)
.await;
}
let deadline = std::time::Instant::now() + DAEMON_TASK_ABORT_DEADLINE;
Expand Down
2 changes: 1 addition & 1 deletion crates/tracedecay/src/daemon/engine/shutdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ impl DaemonEngine {
deadline: std::time::Instant,
) -> ShutdownTaskReceipt {
shutdown_project_servers(
tokio::time::Instant::from_std(deadline),
deadline,
&self.store_administration,
&self.http_application_registry,
)
Expand Down
2 changes: 1 addition & 1 deletion crates/tracedecay/src/daemon/production_harness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1393,7 +1393,7 @@ async fn shutdown_production_project_harness(mut resources: ProductionProjectHar
.await;
tracing::Instrument::instrument(
shutdown_detached_project_servers(
tokio::time::Instant::now() + tracedecay_runtime_core::DAEMON_SHUTDOWN_DEADLINE,
std::time::Instant::now() + tracedecay_runtime_core::DAEMON_SHUTDOWN_DEADLINE,
servers,
),
tracing::trace_span!("daemon.harness.shutdown_detached"),
Expand Down
30 changes: 21 additions & 9 deletions crates/tracedecay/src/daemon/project_server_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@

use super::*;
use std::collections::HashSet;
use std::time::Instant;
use tracedecay_agent_hosts::agents::context_scout::owner::unregister_registered_context_scout_owner;
use tracedecay_agent_hosts::hooks::hook_project_id_for_layout;
use tracedecay_daemon_identity::authority;
use tracedecay_daemon_service::ProfileHostAdmissionBootstrapStatus;
use tracedecay_daemon_service::shutdown::ShutdownStatus;
use tracedecay_runtime_core::logging::log_daemon_event;
use tracedecay_runtime_core::native_runtime::{self, TimeoutError};
use tracedecay_store_runtime::{
ShutdownTaskOutcome, ShutdownTaskReceipt, join_shutdown_tasks_until,
};
Expand All @@ -32,21 +34,31 @@ pub(super) async fn cancel_retained_session_history(store_administration: &Store
/// those owners; typed failures are replayed into every subsequent receipt.
#[tracing::instrument(name = "daemon.project.lifecycle.shutdown", level = "trace", skip_all)]
pub(super) async fn shutdown_project_servers(
deadline: tokio::time::Instant,
deadline: Instant,
store_administration: &StoreAdministration,
http_application_registry: &super::http_application::DaemonHttpApplicationRegistry,
) -> ShutdownTaskReceipt {
http_application_registry
.drain_project_routes_for_shutdown()
.await;
let (detached, mut receipt) =
match tokio::time::timeout_at(deadline, detach_project_servers(store_administration)).await
match native_runtime::timeout_at(deadline, detach_project_servers(store_administration))
.await
{
Ok(servers) => (servers, ShutdownTaskReceipt::default()),
Err(_) => (
Err(TimeoutError::Elapsed) => (
Vec::new(),
ShutdownTaskReceipt::timed_out("project_server_detach"),
),
Err(error @ TimeoutError::TimerUnavailable(_)) => (
Vec::new(),
ShutdownTaskReceipt {
outcomes: vec![ShutdownTaskOutcome {
owner: "project_server_detach".to_owned(),
status: ShutdownStatus::Failed(error.to_string()),
}],
},
),
};
let mut retained = {
let mut owners = store_administration
Expand Down Expand Up @@ -156,7 +168,7 @@ pub(super) async fn detach_project_servers(
skip_all
)]
pub(super) async fn shutdown_detached_project_servers(
deadline: tokio::time::Instant,
deadline: Instant,
servers: Vec<Arc<crate::mcp::McpServer>>,
) -> ShutdownTaskReceipt {
join_shutdown_tasks_until(
Expand Down Expand Up @@ -199,30 +211,30 @@ pub(in crate::daemon) async fn retire_project_servers(
servers: Vec<Arc<crate::mcp::McpServer>>,
route_registered: Option<Arc<AtomicBool>>,
) {
if tokio::time::timeout(
if let Err(error) = native_runtime::timeout(
PROJECT_SERVER_REQUEST_DRAIN_DEADLINE,
wait_for_project_server_request_drains(&servers),
)
.await
.is_err()
{
tracing::warn!(
deadline_secs = PROJECT_SERVER_REQUEST_DRAIN_DEADLINE.as_secs(),
%error,
server_count = servers.len(),
"retired project requests exceeded their drain deadline; cancelling them"
);
for server in &servers {
server.abort_project_server_requests();
}
if tokio::time::timeout(
if let Err(error) = native_runtime::timeout(
PROJECT_SERVER_ABORT_DRAIN_DEADLINE,
wait_for_project_server_request_drains(&servers),
)
.await
.is_err()
{
tracing::warn!(
deadline_secs = PROJECT_SERVER_ABORT_DRAIN_DEADLINE.as_secs(),
%error,
server_count = servers.len(),
"cancelled project requests have not yielded; retaining safe shutdown ownership"
);
Expand Down Expand Up @@ -439,7 +451,7 @@ mod shutdown_owner_tests {

for _ in 0..2 {
let receipt = shutdown_project_servers(
tokio::time::Instant::now() + Duration::from_secs(5),
Instant::now() + Duration::from_secs(5),
&administration,
&super::super::http_application::DaemonHttpApplicationRegistry::default(),
)
Expand Down
Loading
Loading