dsh-ops 0.0.0-stage → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +202 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +435 -0
- package/README.en.md +126 -0
- package/README.md +115 -2
- package/README.zh.md +116 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/release-0.2.1.md +72 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +998 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
//! Immutable response units, exact prefix checkpoints, and one-shot final rendering.
|
|
2
|
+
|
|
3
|
+
use crate::budget::{ExactPrefixCounter, TokenCheckpoint, TokenCountError};
|
|
4
|
+
#[cfg(test)]
|
|
5
|
+
use crate::operation::TestStage;
|
|
6
|
+
use crate::operation::{WorkCheckpoint, WorkStop};
|
|
7
|
+
use std::collections::HashMap;
|
|
8
|
+
use std::fmt;
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
|
|
11
|
+
/// A fully assembled response whose incremental and independent token counts agree.
|
|
12
|
+
#[derive(Debug)]
|
|
13
|
+
pub(crate) struct VerifiedRender {
|
|
14
|
+
pub(crate) text: String,
|
|
15
|
+
pub(crate) tokens: usize,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Failures that must stop output instead of risking a truncated response.
|
|
19
|
+
#[derive(Debug)]
|
|
20
|
+
pub(crate) enum RenderPlanError {
|
|
21
|
+
Token(TokenCountError),
|
|
22
|
+
InvalidPrefix { shown: usize, available: usize },
|
|
23
|
+
InvalidTerminal,
|
|
24
|
+
CountMismatch { incremental: usize, full: usize },
|
|
25
|
+
OverBudget { tokens: usize, budget: usize },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
impl RenderPlanError {
|
|
29
|
+
pub(crate) fn is_cancelled(&self) -> bool {
|
|
30
|
+
matches!(
|
|
31
|
+
self,
|
|
32
|
+
Self::Token(TokenCountError::Stopped(WorkStop::RequestCancelled))
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl fmt::Display for RenderPlanError {
|
|
38
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
39
|
+
match self {
|
|
40
|
+
Self::Token(error) => error.fmt(formatter),
|
|
41
|
+
Self::InvalidPrefix { shown, available } => write!(
|
|
42
|
+
formatter,
|
|
43
|
+
"The renderer selected {shown} entries from only {available} available entries."
|
|
44
|
+
),
|
|
45
|
+
Self::InvalidTerminal => formatter.write_str(
|
|
46
|
+
"The renderer received a terminal note outside the grep compatibility grammar.",
|
|
47
|
+
),
|
|
48
|
+
Self::CountMismatch { incremental, full } => write!(
|
|
49
|
+
formatter,
|
|
50
|
+
"Internal token-count invariant failed: incremental={incremental}, full={full}."
|
|
51
|
+
),
|
|
52
|
+
Self::OverBudget { tokens, budget } => write!(
|
|
53
|
+
formatter,
|
|
54
|
+
"The selected render uses {tokens} tokens but its budget is {budget}."
|
|
55
|
+
),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl From<TokenCountError> for RenderPlanError {
|
|
61
|
+
fn from(error: TokenCountError) -> Self {
|
|
62
|
+
Self::Token(error)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Lines rendered exactly once, with an exact tokenizer checkpoint after every prefix.
|
|
67
|
+
pub(crate) struct LineRenderGraph {
|
|
68
|
+
lines: Vec<Arc<str>>,
|
|
69
|
+
checkpoints: Vec<TokenCheckpoint>,
|
|
70
|
+
counter: ExactPrefixCounter,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl LineRenderGraph {
|
|
74
|
+
pub(crate) fn new(
|
|
75
|
+
lines: Vec<Arc<str>>,
|
|
76
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
77
|
+
) -> Result<Self, RenderPlanError> {
|
|
78
|
+
let mut counter = ExactPrefixCounter::default();
|
|
79
|
+
let mut checkpoints = Vec::with_capacity(lines.len().saturating_add(1));
|
|
80
|
+
checkpoints.push(counter.checkpoint());
|
|
81
|
+
for (index, line) in lines.iter().enumerate() {
|
|
82
|
+
check_render_work(operation, TestRenderStage::Unit)?;
|
|
83
|
+
if index > 0 {
|
|
84
|
+
counter.append("\n", operation)?;
|
|
85
|
+
}
|
|
86
|
+
counter.append(line, operation)?;
|
|
87
|
+
checkpoints.push(counter.checkpoint());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Ok(Self {
|
|
91
|
+
lines,
|
|
92
|
+
checkpoints,
|
|
93
|
+
counter,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Returns the immutable tokenizer state at one body-entry prefix.
|
|
98
|
+
pub(crate) fn checkpoint(&self, shown: usize) -> Result<TokenCheckpoint, RenderPlanError> {
|
|
99
|
+
self.checkpoints
|
|
100
|
+
.get(shown)
|
|
101
|
+
.cloned()
|
|
102
|
+
.ok_or(RenderPlanError::InvalidPrefix {
|
|
103
|
+
shown,
|
|
104
|
+
available: self.lines.len(),
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Counts a prefix plus its notes using only the checkpoint tail and short trailer.
|
|
109
|
+
pub(crate) fn probe_notes<T: AsRef<str>>(
|
|
110
|
+
&mut self,
|
|
111
|
+
shown: usize,
|
|
112
|
+
notes: &[T],
|
|
113
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
114
|
+
) -> Result<usize, RenderPlanError> {
|
|
115
|
+
check_render_work(operation, TestRenderStage::TokenProbe)?;
|
|
116
|
+
let checkpoint = self
|
|
117
|
+
.checkpoints
|
|
118
|
+
.get(shown)
|
|
119
|
+
.ok_or(RenderPlanError::InvalidPrefix {
|
|
120
|
+
shown,
|
|
121
|
+
available: self.lines.len(),
|
|
122
|
+
})?;
|
|
123
|
+
let trailer = render_notes_suffix(shown, notes);
|
|
124
|
+
self.counter
|
|
125
|
+
.count_with_suffix(checkpoint, &trailer, operation)
|
|
126
|
+
.map_err(Into::into)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/// Assembles the selected view once, then independently verifies the full text once.
|
|
130
|
+
pub(crate) fn finish<T: AsRef<str>>(
|
|
131
|
+
&mut self,
|
|
132
|
+
shown: usize,
|
|
133
|
+
notes: &[T],
|
|
134
|
+
incremental_tokens: usize,
|
|
135
|
+
budget: usize,
|
|
136
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
137
|
+
) -> Result<VerifiedRender, RenderPlanError> {
|
|
138
|
+
if shown > self.lines.len() {
|
|
139
|
+
return Err(RenderPlanError::InvalidPrefix {
|
|
140
|
+
shown,
|
|
141
|
+
available: self.lines.len(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
check_render_work(operation, TestRenderStage::Unit)?;
|
|
145
|
+
let mut text = String::new();
|
|
146
|
+
for (index, line) in self.lines[..shown].iter().enumerate() {
|
|
147
|
+
if index > 0 {
|
|
148
|
+
text.push('\n');
|
|
149
|
+
}
|
|
150
|
+
text.push_str(line);
|
|
151
|
+
}
|
|
152
|
+
if !notes.is_empty() {
|
|
153
|
+
if shown > 0 {
|
|
154
|
+
text.push_str("\n\n");
|
|
155
|
+
}
|
|
156
|
+
for (index, note) in notes.iter().enumerate() {
|
|
157
|
+
if index > 0 {
|
|
158
|
+
text.push('\n');
|
|
159
|
+
}
|
|
160
|
+
text.push_str(note.as_ref());
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
check_render_work(operation, TestRenderStage::FinalVerify)?;
|
|
165
|
+
let full_tokens = self.counter.verify_full(&text, operation)?;
|
|
166
|
+
if full_tokens != incremental_tokens {
|
|
167
|
+
return Err(RenderPlanError::CountMismatch {
|
|
168
|
+
incremental: incremental_tokens,
|
|
169
|
+
full: full_tokens,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
if full_tokens > budget {
|
|
173
|
+
return Err(RenderPlanError::OverBudget {
|
|
174
|
+
tokens: full_tokens,
|
|
175
|
+
budget,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
Ok(VerifiedRender {
|
|
179
|
+
text,
|
|
180
|
+
tokens: full_tokens,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#[derive(Clone)]
|
|
186
|
+
pub(crate) struct LineRenderView {
|
|
187
|
+
lines: Arc<[Arc<str>]>,
|
|
188
|
+
checkpoint: TokenCheckpoint,
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
impl LineRenderView {
|
|
192
|
+
pub(crate) fn len(&self) -> usize {
|
|
193
|
+
self.lines.len()
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
pub(crate) fn checkpoint(&self) -> &TokenCheckpoint {
|
|
197
|
+
&self.checkpoint
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
struct SharedPrefixNode {
|
|
202
|
+
checkpoint: TokenCheckpoint,
|
|
203
|
+
children: HashMap<Arc<str>, usize>,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// A request-local prefix trie for multiple compatibility views whose line
|
|
207
|
+
/// sequences overlap but are not necessarily prefixes of one maximum view.
|
|
208
|
+
pub(crate) struct SharedLineRenderGraph {
|
|
209
|
+
nodes: Vec<SharedPrefixNode>,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
impl SharedLineRenderGraph {
|
|
213
|
+
pub(crate) fn new() -> Self {
|
|
214
|
+
let counter = ExactPrefixCounter::default();
|
|
215
|
+
Self {
|
|
216
|
+
nodes: vec![SharedPrefixNode {
|
|
217
|
+
checkpoint: counter.checkpoint(),
|
|
218
|
+
children: HashMap::new(),
|
|
219
|
+
}],
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Interns one immutable line view, tokenizing only prefix edges that no
|
|
224
|
+
/// earlier compatibility probe has already established.
|
|
225
|
+
pub(crate) fn prepare_view(
|
|
226
|
+
&mut self,
|
|
227
|
+
lines: Vec<Arc<str>>,
|
|
228
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
229
|
+
) -> Result<LineRenderView, RenderPlanError> {
|
|
230
|
+
let mut node_index = 0_usize;
|
|
231
|
+
for (depth, line) in lines.iter().enumerate() {
|
|
232
|
+
check_render_work(operation, TestRenderStage::Unit)?;
|
|
233
|
+
if let Some(child) = self.nodes[node_index].children.get(line).copied() {
|
|
234
|
+
node_index = child;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let parent_checkpoint = self.nodes[node_index].checkpoint.clone();
|
|
239
|
+
let mut counter = ExactPrefixCounter::from_checkpoint(&parent_checkpoint);
|
|
240
|
+
if depth > 0 {
|
|
241
|
+
counter.append("\n", operation)?;
|
|
242
|
+
}
|
|
243
|
+
counter.append(line, operation)?;
|
|
244
|
+
let child = self.nodes.len();
|
|
245
|
+
self.nodes.push(SharedPrefixNode {
|
|
246
|
+
checkpoint: counter.checkpoint(),
|
|
247
|
+
children: HashMap::new(),
|
|
248
|
+
});
|
|
249
|
+
self.nodes[node_index]
|
|
250
|
+
.children
|
|
251
|
+
.insert(Arc::clone(line), child);
|
|
252
|
+
node_index = child;
|
|
253
|
+
}
|
|
254
|
+
Ok(LineRenderView {
|
|
255
|
+
lines: Arc::from(lines),
|
|
256
|
+
checkpoint: self.nodes[node_index].checkpoint.clone(),
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
pub(crate) fn probe_notes<T: AsRef<str>>(
|
|
261
|
+
&mut self,
|
|
262
|
+
view: &LineRenderView,
|
|
263
|
+
notes: &[T],
|
|
264
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
265
|
+
) -> Result<usize, RenderPlanError> {
|
|
266
|
+
check_render_work(operation, TestRenderStage::TokenProbe)?;
|
|
267
|
+
let suffix = render_notes_suffix(view.len(), notes);
|
|
268
|
+
let mut counter = ExactPrefixCounter::from_checkpoint(&view.checkpoint);
|
|
269
|
+
counter
|
|
270
|
+
.count_with_suffix(&view.checkpoint, &suffix, operation)
|
|
271
|
+
.map_err(Into::into)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
pub(crate) fn finish<T: AsRef<str>>(
|
|
275
|
+
&mut self,
|
|
276
|
+
view: &LineRenderView,
|
|
277
|
+
notes: &[T],
|
|
278
|
+
incremental_tokens: usize,
|
|
279
|
+
budget: usize,
|
|
280
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
281
|
+
) -> Result<VerifiedRender, RenderPlanError> {
|
|
282
|
+
check_render_work(operation, TestRenderStage::Unit)?;
|
|
283
|
+
let mut text = String::new();
|
|
284
|
+
for (index, line) in view.lines.iter().enumerate() {
|
|
285
|
+
if index > 0 {
|
|
286
|
+
text.push('\n');
|
|
287
|
+
}
|
|
288
|
+
text.push_str(line);
|
|
289
|
+
}
|
|
290
|
+
if !notes.is_empty() {
|
|
291
|
+
if !view.lines.is_empty() {
|
|
292
|
+
text.push_str("\n\n");
|
|
293
|
+
}
|
|
294
|
+
for (index, note) in notes.iter().enumerate() {
|
|
295
|
+
if index > 0 {
|
|
296
|
+
text.push('\n');
|
|
297
|
+
}
|
|
298
|
+
text.push_str(note.as_ref());
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
check_render_work(operation, TestRenderStage::FinalVerify)?;
|
|
303
|
+
let full_tokens = crate::budget::estimate_tokens(&text);
|
|
304
|
+
check_render_work(operation, TestRenderStage::FinalVerify)?;
|
|
305
|
+
if full_tokens != incremental_tokens {
|
|
306
|
+
return Err(RenderPlanError::CountMismatch {
|
|
307
|
+
incremental: incremental_tokens,
|
|
308
|
+
full: full_tokens,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if full_tokens > budget {
|
|
312
|
+
return Err(RenderPlanError::OverBudget {
|
|
313
|
+
tokens: full_tokens,
|
|
314
|
+
budget,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
Ok(VerifiedRender {
|
|
318
|
+
text,
|
|
319
|
+
tokens: full_tokens,
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// Exact checkpoints for an optional prefix of diagnostic detail lines after a fixed body.
|
|
325
|
+
pub(crate) struct DetailRenderGraph {
|
|
326
|
+
prefix_has_body: bool,
|
|
327
|
+
fixed_lines: usize,
|
|
328
|
+
detail_lines: usize,
|
|
329
|
+
checkpoints: Vec<TokenCheckpoint>,
|
|
330
|
+
counter: ExactPrefixCounter,
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
impl DetailRenderGraph {
|
|
334
|
+
pub(crate) fn new(
|
|
335
|
+
body_checkpoint: &TokenCheckpoint,
|
|
336
|
+
prefix_has_body: bool,
|
|
337
|
+
fixed_lines: &[Arc<str>],
|
|
338
|
+
detail_lines: &[Arc<str>],
|
|
339
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
340
|
+
) -> Result<Self, RenderPlanError> {
|
|
341
|
+
let mut counter = ExactPrefixCounter::from_checkpoint(body_checkpoint);
|
|
342
|
+
let mut note_count = 0_usize;
|
|
343
|
+
for line in fixed_lines {
|
|
344
|
+
append_note_line(&mut counter, line, prefix_has_body, note_count, operation)?;
|
|
345
|
+
note_count = note_count.saturating_add(1);
|
|
346
|
+
}
|
|
347
|
+
let mut checkpoints = Vec::with_capacity(detail_lines.len().saturating_add(1));
|
|
348
|
+
checkpoints.push(counter.checkpoint());
|
|
349
|
+
for line in detail_lines {
|
|
350
|
+
append_note_line(&mut counter, line, prefix_has_body, note_count, operation)?;
|
|
351
|
+
note_count = note_count.saturating_add(1);
|
|
352
|
+
checkpoints.push(counter.checkpoint());
|
|
353
|
+
}
|
|
354
|
+
Ok(Self {
|
|
355
|
+
prefix_has_body,
|
|
356
|
+
fixed_lines: fixed_lines.len(),
|
|
357
|
+
detail_lines: detail_lines.len(),
|
|
358
|
+
checkpoints,
|
|
359
|
+
counter,
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// Counts the selected detail prefix plus mandatory trailing note lines.
|
|
364
|
+
pub(crate) fn probe_tail<T: AsRef<str>>(
|
|
365
|
+
&mut self,
|
|
366
|
+
shown_details: usize,
|
|
367
|
+
tail_lines: &[T],
|
|
368
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
369
|
+
) -> Result<usize, RenderPlanError> {
|
|
370
|
+
check_render_work(operation, TestRenderStage::TokenProbe)?;
|
|
371
|
+
let checkpoint =
|
|
372
|
+
self.checkpoints
|
|
373
|
+
.get(shown_details)
|
|
374
|
+
.ok_or(RenderPlanError::InvalidPrefix {
|
|
375
|
+
shown: shown_details,
|
|
376
|
+
available: self.detail_lines,
|
|
377
|
+
})?;
|
|
378
|
+
let existing_notes = self.fixed_lines.saturating_add(shown_details);
|
|
379
|
+
let suffix = render_continuation_suffix(self.prefix_has_body, existing_notes, tail_lines);
|
|
380
|
+
self.counter
|
|
381
|
+
.count_with_suffix(checkpoint, &suffix, operation)
|
|
382
|
+
.map_err(Into::into)
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
fn append_note_line(
|
|
387
|
+
counter: &mut ExactPrefixCounter,
|
|
388
|
+
line: &str,
|
|
389
|
+
prefix_has_body: bool,
|
|
390
|
+
existing_notes: usize,
|
|
391
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
392
|
+
) -> Result<(), RenderPlanError> {
|
|
393
|
+
if existing_notes > 0 {
|
|
394
|
+
counter.append("\n", operation)?;
|
|
395
|
+
} else if prefix_has_body {
|
|
396
|
+
counter.append("\n\n", operation)?;
|
|
397
|
+
}
|
|
398
|
+
counter.append(line, operation)?;
|
|
399
|
+
Ok(())
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
fn render_continuation_suffix<T: AsRef<str>>(
|
|
403
|
+
prefix_has_body: bool,
|
|
404
|
+
existing_notes: usize,
|
|
405
|
+
lines: &[T],
|
|
406
|
+
) -> String {
|
|
407
|
+
if lines.is_empty() {
|
|
408
|
+
return String::new();
|
|
409
|
+
}
|
|
410
|
+
let mut suffix = String::new();
|
|
411
|
+
if existing_notes > 0 {
|
|
412
|
+
suffix.push('\n');
|
|
413
|
+
} else if prefix_has_body {
|
|
414
|
+
suffix.push_str("\n\n");
|
|
415
|
+
}
|
|
416
|
+
for (index, line) in lines.iter().enumerate() {
|
|
417
|
+
if index > 0 {
|
|
418
|
+
suffix.push('\n');
|
|
419
|
+
}
|
|
420
|
+
suffix.push_str(line.as_ref());
|
|
421
|
+
}
|
|
422
|
+
suffix
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
fn render_notes_suffix<T: AsRef<str>>(shown: usize, notes: &[T]) -> String {
|
|
426
|
+
if notes.is_empty() {
|
|
427
|
+
return String::new();
|
|
428
|
+
}
|
|
429
|
+
let mut suffix = String::new();
|
|
430
|
+
if shown > 0 {
|
|
431
|
+
suffix.push_str("\n\n");
|
|
432
|
+
}
|
|
433
|
+
for (index, note) in notes.iter().enumerate() {
|
|
434
|
+
if index > 0 {
|
|
435
|
+
suffix.push('\n');
|
|
436
|
+
}
|
|
437
|
+
suffix.push_str(note.as_ref());
|
|
438
|
+
}
|
|
439
|
+
suffix
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
#[derive(Clone, Copy)]
|
|
443
|
+
enum TestRenderStage {
|
|
444
|
+
Unit,
|
|
445
|
+
TokenProbe,
|
|
446
|
+
FinalVerify,
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
fn check_render_work(
|
|
450
|
+
operation: Option<&dyn WorkCheckpoint>,
|
|
451
|
+
stage: TestRenderStage,
|
|
452
|
+
) -> Result<(), RenderPlanError> {
|
|
453
|
+
if let Some(operation) = operation {
|
|
454
|
+
operation.check_work().map_err(TokenCountError::Stopped)?;
|
|
455
|
+
#[cfg(test)]
|
|
456
|
+
operation.stage(match stage {
|
|
457
|
+
TestRenderStage::Unit => TestStage::RenderUnit,
|
|
458
|
+
TestRenderStage::TokenProbe => TestStage::TokenProbe,
|
|
459
|
+
TestRenderStage::FinalVerify => TestStage::BeforeFinalTokenVerify,
|
|
460
|
+
});
|
|
461
|
+
#[cfg(not(test))]
|
|
462
|
+
let _ = stage;
|
|
463
|
+
operation.check_work().map_err(TokenCountError::Stopped)?;
|
|
464
|
+
} else {
|
|
465
|
+
let _ = stage;
|
|
466
|
+
}
|
|
467
|
+
Ok(())
|
|
468
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
//! Per-user connection and request activity used by the control center's shutdown policy.
|
|
2
|
+
|
|
3
|
+
use std::sync::{Arc, Mutex};
|
|
4
|
+
use std::time::{Duration, Instant};
|
|
5
|
+
|
|
6
|
+
#[derive(Debug)]
|
|
7
|
+
struct ActivityState {
|
|
8
|
+
accepting_connections: bool,
|
|
9
|
+
connections: usize,
|
|
10
|
+
in_flight: usize,
|
|
11
|
+
last_activity: Instant,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Shared connection gate, counters, and last-activity timestamp for one control center.
|
|
15
|
+
#[derive(Debug)]
|
|
16
|
+
pub(crate) struct RuntimeActivity {
|
|
17
|
+
state: Mutex<ActivityState>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl RuntimeActivity {
|
|
21
|
+
pub(crate) fn new() -> Arc<Self> {
|
|
22
|
+
Arc::new(Self {
|
|
23
|
+
state: Mutex::new(ActivityState {
|
|
24
|
+
accepting_connections: true,
|
|
25
|
+
connections: 0,
|
|
26
|
+
in_flight: 0,
|
|
27
|
+
last_activity: Instant::now(),
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub(crate) fn try_connection(self: &Arc<Self>) -> Option<ConnectionActivityGuard> {
|
|
33
|
+
let mut state = self.state.lock().unwrap();
|
|
34
|
+
if !state.accepting_connections {
|
|
35
|
+
return None;
|
|
36
|
+
}
|
|
37
|
+
state.connections = state.connections.saturating_add(1);
|
|
38
|
+
state.last_activity = Instant::now();
|
|
39
|
+
Some(ConnectionActivityGuard {
|
|
40
|
+
activity: Arc::clone(self),
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pub(crate) fn request(self: &Arc<Self>) -> RequestActivityGuard {
|
|
45
|
+
let mut state = self.state.lock().unwrap();
|
|
46
|
+
state.in_flight = state.in_flight.saturating_add(1);
|
|
47
|
+
state.last_activity = Instant::now();
|
|
48
|
+
RequestActivityGuard {
|
|
49
|
+
activity: Arc::clone(self),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(crate) fn touch(&self) {
|
|
54
|
+
self.state.lock().unwrap().last_activity = Instant::now();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub(crate) fn is_shutdown_eligible(&self, duration: Duration) -> bool {
|
|
58
|
+
self.is_shutdown_eligible_at(Instant::now(), duration)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pub(crate) fn try_begin_shutdown(&self, duration: Duration) -> bool {
|
|
62
|
+
self.try_begin_shutdown_at(Instant::now(), duration)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn is_shutdown_eligible_at(&self, now: Instant, duration: Duration) -> bool {
|
|
66
|
+
let state = self.state.lock().unwrap();
|
|
67
|
+
shutdown_eligible(&state, now, duration)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn try_begin_shutdown_at(&self, now: Instant, duration: Duration) -> bool {
|
|
71
|
+
let mut state = self.state.lock().unwrap();
|
|
72
|
+
if !state.accepting_connections || !shutdown_eligible(&state, now, duration) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
state.accepting_connections = false;
|
|
76
|
+
true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn shutdown_eligible(state: &ActivityState, now: Instant, duration: Duration) -> bool {
|
|
81
|
+
state.connections == 0
|
|
82
|
+
&& state.in_flight == 0
|
|
83
|
+
&& now.saturating_duration_since(state.last_activity) >= duration
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// One accepted IPC connection, including its handshake. Dropping the lease restarts idle time.
|
|
87
|
+
pub(crate) struct ConnectionActivityGuard {
|
|
88
|
+
activity: Arc<RuntimeActivity>,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
impl Drop for ConnectionActivityGuard {
|
|
92
|
+
fn drop(&mut self) {
|
|
93
|
+
let mut state = self.activity.state.lock().unwrap();
|
|
94
|
+
state.connections = state.connections.saturating_sub(1);
|
|
95
|
+
state.last_activity = Instant::now();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// One in-flight request. Dropping it records completion even on cancellation or panic unwind.
|
|
100
|
+
pub(crate) struct RequestActivityGuard {
|
|
101
|
+
activity: Arc<RuntimeActivity>,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl Drop for RequestActivityGuard {
|
|
105
|
+
fn drop(&mut self) {
|
|
106
|
+
let mut state = self.activity.state.lock().unwrap();
|
|
107
|
+
state.in_flight = state.in_flight.saturating_sub(1);
|
|
108
|
+
state.last_activity = Instant::now();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[cfg(test)]
|
|
113
|
+
mod tests {
|
|
114
|
+
use super::RuntimeActivity;
|
|
115
|
+
use std::time::{Duration, Instant};
|
|
116
|
+
|
|
117
|
+
#[test]
|
|
118
|
+
fn in_flight_work_prevents_idle_and_completion_restarts_the_clock() {
|
|
119
|
+
let activity = RuntimeActivity::new();
|
|
120
|
+
let guard = activity.request();
|
|
121
|
+
assert!(!activity.is_shutdown_eligible_at(
|
|
122
|
+
Instant::now() + Duration::from_secs(60),
|
|
123
|
+
Duration::from_secs(1)
|
|
124
|
+
));
|
|
125
|
+
drop(guard);
|
|
126
|
+
assert!(!activity.is_shutdown_eligible_at(Instant::now(), Duration::from_secs(1)));
|
|
127
|
+
assert!(activity.is_shutdown_eligible_at(
|
|
128
|
+
Instant::now() + Duration::from_secs(2),
|
|
129
|
+
Duration::from_secs(1)
|
|
130
|
+
));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[test]
|
|
134
|
+
fn live_connection_prevents_shutdown_and_disconnect_restarts_the_clock() {
|
|
135
|
+
let activity = RuntimeActivity::new();
|
|
136
|
+
let connection = activity.try_connection().unwrap();
|
|
137
|
+
assert!(!activity.is_shutdown_eligible_at(
|
|
138
|
+
Instant::now() + Duration::from_secs(60),
|
|
139
|
+
Duration::from_secs(1)
|
|
140
|
+
));
|
|
141
|
+
drop(connection);
|
|
142
|
+
assert!(!activity.is_shutdown_eligible_at(Instant::now(), Duration::from_secs(1)));
|
|
143
|
+
assert!(activity.is_shutdown_eligible_at(
|
|
144
|
+
Instant::now() + Duration::from_secs(2),
|
|
145
|
+
Duration::from_secs(1)
|
|
146
|
+
));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#[test]
|
|
150
|
+
fn shutdown_decision_closes_the_same_gate_used_by_accept() {
|
|
151
|
+
let activity = RuntimeActivity::new();
|
|
152
|
+
assert!(activity.try_begin_shutdown_at(
|
|
153
|
+
Instant::now() + Duration::from_secs(2),
|
|
154
|
+
Duration::from_secs(1)
|
|
155
|
+
));
|
|
156
|
+
assert!(activity.try_connection().is_none());
|
|
157
|
+
assert!(!activity.try_begin_shutdown(Duration::ZERO));
|
|
158
|
+
}
|
|
159
|
+
}
|