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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
//! Running the engine for one channel, and the instrumented lock accessors
//! every caller goes through to reach it.
use Arc;
use Duration;
use ArcSwap;
use profile;
/// The live engine, swapped wholesale on reload.
///
/// Was `Arc<RwLock<Arc<Engine>>>`. The outer lock never protected a mutation —
/// reload builds the replacement engine entirely outside it and the critical
/// section was a single assignment — so all it ever did was serialise readers
/// against a writer that had nothing left to do. Every data-plane request paid
/// a futures-aware acquire, and a reload could still block readers in the
/// window between the timeout and the store.
///
/// `ArcSwap` is the shape the access pattern always had: many readers taking a
/// snapshot, one writer publishing a finished value. Readers never block and
/// never wait, so the reload no longer needs a timeout to bound how long it
/// might hold them off, and a reader that is mid-request keeps the engine it
/// started with until it drops the `Arc`. N17 made the same change to the
/// channel-registry snapshot for the same reason.
///
/// Reloads are still serialised, by the `reload_lock` in `AppState` — that is
/// a separate concern (two concurrent reloads would each build from a possibly
/// stale read) and is not what this type is for.
;
/// Result of one engine invocation: the engine's own result plus the captured
/// per-task `ExecutionTrace` when the caller opted in (A2).
pub type EngineCallResult = ;
/// Opt in to per-task trace capture, bounded by the same byte budget the
/// persisted row is capped at.
///
/// A `bool` before: the engine took no capture policy, so the only defence
/// against an oversized trace was throwing the finished one away.
/// Run the engine for `channel` with optional timeout, optional per-task
/// trace capture, and optional profiling scope. The sync HTTP, async trace
/// queue, Kafka ingress and in-process `channel_call` paths all go through
/// here so timeout and trace semantics cannot drift between them. `Err(ms)`
/// means the call timed out after `ms` milliseconds.
pub async
/// Await `fut` under an optional deadline, reporting the elapsed budget as the
/// error so the caller can name it in a 504.
///
/// F46: the timeout arm used to be written out once per (capture_trace ×
/// timeout) combination — four branches for two independent choices, where the
/// timed and untimed halves of each pair could drift apart silently. It is one
/// decision, made here.
async
/// What a `task_details` run records per executed step.
///
/// Under the default policy a step deep-clones the whole `Message` — context,
/// payload **and** the accumulated audit trail — so trace size is unbounded in
/// message size and quadratic in task count: a 6-task workflow over a ~1 MB
/// context serialized to ~12 MB. `serialize_task_trace_capped` is the exact
/// cap, but it runs strictly afterwards, by which point the clones and the
/// serialization are already paid. This bounds it at capture time, which is the
/// only place memory can be bounded.
///
/// - `snapshot_audit_trail: Own` — each snapshot carries only the entry its own
/// task produced. `Full` accumulates `N*(N+1)/2` entries across a trace and is
/// the term that makes the growth quadratic. Orion reads `Message::audit_trail`
/// nowhere.
/// - `changes` — the per-task diff, which is what `task_details` is *for*
/// ("inspect intermediate inputs/outputs for each task"). Correctly attributed
/// on a `Skip`, unlike reading `audit_trail.last()`.
/// - `redact_paths: ["metadata.headers", "metadata.cookies"]` — a pruning
/// clone, so neither map is cloned into a step in the first place.
/// `context.metadata` is stripped from `result_json` on read (S14) but
/// `task_trace_json` was returned verbatim, and every step inside it held a
/// full `Message` clone carrying the same headers. Only four header names are
/// masked at ingress, so everything else was readable through that hole.
/// Forward-only: rows already on disk still need the read-side strip.
///
/// This is a **path list, not a metadata-wide prune**, so a new metadata key
/// is covered only by being named here — which is why `metadata.cookies`
/// (#270) had to be added when the cookie allowlist landed, or an
/// allowlisted value would be cloned into every step snapshot and persisted
/// for any channel with `tracing.task_details = true`.
///
/// Note what this defends: the row **at rest**. The trace read already
/// strips `context.metadata` whole-message and per-step, so a missing entry
/// here does not surface through the API — which is exactly why it needs
/// stating rather than testing end-to-end.
async