1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0
// Raised from the default 128 to accommodate deeply nested async state machines
// generated by #[tracing::instrument] on the agent call stack.
#![recursion_limit = "256"]
// Thread-local allocation counter used by AllocLayer for per-span heap tracking.
// All unsafe code in the project is confined to this module (global allocator declaration).
#[cfg(feature = "profiling-alloc")]
#[allow(unsafe_code)]
mod alloc_counter {
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::RefCell;
/// Global allocator that records per-thread allocation counts and bytes.
///
/// All allocation and deallocation operations are forwarded unchanged to the
/// system allocator. The only addition is a thread-local counter update, which
/// uses `const`-initialised storage to avoid re-entrant allocation on first access.
pub struct CountingAllocator;
// SAFETY: All methods delegate to `System` with identical arguments.
// Thread-local counter updates use `const`-initialised `RefCell` in `.tdata`/`.tbss`,
// so no dynamic allocation occurs on first thread-local access, eliminating allocator
// re-entrancy. Borrows are non-overlapping: each function borrow-mutably, completes,
// and drops the guard before returning.
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
increment_alloc(layout.size());
// SAFETY: forwarding to System with the same layout.
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
increment_dealloc(layout.size());
// SAFETY: forwarding to System with the same pointer and layout.
unsafe { System.dealloc(ptr, layout) }
}
}
#[derive(Clone, Copy)]
struct RawStats {
alloc_count: u64,
alloc_bytes: u64,
dealloc_count: u64,
dealloc_bytes: u64,
}
impl RawStats {
const ZERO: Self = Self {
alloc_count: 0,
alloc_bytes: 0,
dealloc_count: 0,
dealloc_bytes: 0,
};
}
// `const` initialisation places the slot in `.tdata`/`.tbss` — no heap allocation
// on first access, preventing re-entrant calls back into this allocator.
thread_local! {
static STATS: RefCell<RawStats> = const { RefCell::new(RawStats::ZERO) };
}
fn increment_alloc(size: usize) {
STATS.with(|s| {
let mut s = s.borrow_mut();
s.alloc_count += 1;
s.alloc_bytes += size as u64;
});
}
fn increment_dealloc(size: usize) {
STATS.with(|s| {
let mut s = s.borrow_mut();
s.dealloc_count += 1;
s.dealloc_bytes += size as u64;
});
}
/// Snapshot the current thread's allocation counters.
///
/// Returns `(alloc_count, alloc_bytes, dealloc_count, dealloc_bytes)`.
/// Counters are monotonically increasing per-thread and are never reset.
/// `AllocLayer` computes deltas by subtracting the enter snapshot from the exit snapshot.
pub fn snapshot() -> (u64, u64, u64, u64) {
STATS.with(|s| {
let s = s.borrow();
(
s.alloc_count,
s.alloc_bytes,
s.dealloc_count,
s.dealloc_bytes,
)
})
}
}
#[cfg(feature = "profiling-alloc")]
#[allow(unsafe_code)]
#[global_allocator]
static GLOBAL: alloc_counter::CountingAllocator = alloc_counter::CountingAllocator;
mod acp;
mod agent_setup;
mod bootstrap;
mod channel;
#[cfg(feature = "otel")]
mod circuit_breaker_exporter;
mod cli;
mod commands;
mod daemon;
mod db_url;
mod execution_mode;
mod gateway_spawn;
mod init;
#[cfg(feature = "prometheus")]
mod metrics_export;
#[cfg(feature = "profiling-pyroscope")]
mod pyroscope_push;
#[cfg(feature = "otel")]
mod redacting_span_processor;
mod runner;
mod scheduler;
#[cfg(feature = "scheduler")]
mod scheduler_executor;
mod startup_checks;
mod tracing_init;
mod tui_bridge;
mod tui_remote;
use clap::Parser;
use cli::Cli;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
Box::pin(runner::run(Cli::parse())).await
}
#[cfg(test)]
mod tests;