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.
Files changed (140) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LICENSE +30 -0
  3. package/NOTICE +106 -0
  4. package/PROVENANCE.md +435 -0
  5. package/README.en.md +126 -0
  6. package/README.md +115 -2
  7. package/README.zh.md +116 -0
  8. package/bin/dsh-ops.mjs +1216 -0
  9. package/cordis.patch.yml +160 -0
  10. package/docs/manual-validation.md +53 -0
  11. package/docs/release-0.2.1.md +72 -0
  12. package/docs/schema-baseline.json +64 -0
  13. package/docs/schema-current.json +84 -0
  14. package/docs/schema-measurement.md +17 -0
  15. package/dsh-plugin.json +88 -0
  16. package/icon.svg +12 -0
  17. package/lib/binary.js +409 -0
  18. package/lib/config.js +198 -0
  19. package/lib/handshake.js +252 -0
  20. package/lib/index.js +108 -0
  21. package/lib/jobs.js +42 -0
  22. package/lib/policy.js +64 -0
  23. package/lib/presentation.js +63 -0
  24. package/lib/profile-install.js +61 -0
  25. package/lib/rust.js +194 -0
  26. package/lib/session-shells.js +78 -0
  27. package/lib/shells.js +998 -0
  28. package/lib/tools.js +657 -0
  29. package/locale/en.json +6 -0
  30. package/locale/zh.json +6 -0
  31. package/package.json +114 -4
  32. package/vendor/fastctx/Cargo.lock +3210 -0
  33. package/vendor/fastctx/Cargo.toml +94 -0
  34. package/vendor/fastctx/FORK.md +119 -0
  35. package/vendor/fastctx/LICENSE-APACHE +201 -0
  36. package/vendor/fastctx/NOTICE +40 -0
  37. package/vendor/fastctx/README.md +439 -0
  38. package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
  39. package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
  40. package/vendor/fastctx/UPSTREAM.md +49 -0
  41. package/vendor/fastctx/build.rs +413 -0
  42. package/vendor/fastctx/src/background_status.rs +403 -0
  43. package/vendor/fastctx/src/binary.rs +75 -0
  44. package/vendor/fastctx/src/bounded_sort.rs +500 -0
  45. package/vendor/fastctx/src/budget.rs +781 -0
  46. package/vendor/fastctx/src/cli/mod.rs +110 -0
  47. package/vendor/fastctx/src/context_guard.rs +289 -0
  48. package/vendor/fastctx/src/control/mod.rs +6 -0
  49. package/vendor/fastctx/src/control/paths.rs +49 -0
  50. package/vendor/fastctx/src/control/settings.rs +753 -0
  51. package/vendor/fastctx/src/control/transaction.rs +531 -0
  52. package/vendor/fastctx/src/edit/document.rs +535 -0
  53. package/vendor/fastctx/src/edit/locks.rs +371 -0
  54. package/vendor/fastctx/src/edit/mod.rs +213 -0
  55. package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
  56. package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
  57. package/vendor/fastctx/src/edit/private_storage.rs +234 -0
  58. package/vendor/fastctx/src/edit/replace.rs +1030 -0
  59. package/vendor/fastctx/src/edit_server.rs +53 -0
  60. package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
  61. package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
  62. package/vendor/fastctx/src/encoding.rs +1118 -0
  63. package/vendor/fastctx/src/file_executor.rs +1151 -0
  64. package/vendor/fastctx/src/file_snapshot.rs +1491 -0
  65. package/vendor/fastctx/src/glob_filter.rs +98 -0
  66. package/vendor/fastctx/src/glob_tool.rs +653 -0
  67. package/vendor/fastctx/src/grep_sink.rs +1162 -0
  68. package/vendor/fastctx/src/grep_tool.rs +2449 -0
  69. package/vendor/fastctx/src/lib.rs +45 -0
  70. package/vendor/fastctx/src/main.rs +15 -0
  71. package/vendor/fastctx/src/model.rs +51 -0
  72. package/vendor/fastctx/src/model_guidance.rs +62 -0
  73. package/vendor/fastctx/src/operation.rs +356 -0
  74. package/vendor/fastctx/src/ordered_window.rs +1235 -0
  75. package/vendor/fastctx/src/os_environment.rs +414 -0
  76. package/vendor/fastctx/src/path_codec.rs +850 -0
  77. package/vendor/fastctx/src/paths.rs +244 -0
  78. package/vendor/fastctx/src/process_identity.rs +763 -0
  79. package/vendor/fastctx/src/process_policy.rs +74 -0
  80. package/vendor/fastctx/src/read_tool/batch.rs +496 -0
  81. package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
  82. package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
  83. package/vendor/fastctx/src/read_tool/mod.rs +245 -0
  84. package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
  85. package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
  86. package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
  87. package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
  88. package/vendor/fastctx/src/render_plan.rs +468 -0
  89. package/vendor/fastctx/src/runtime/activity.rs +159 -0
  90. package/vendor/fastctx/src/runtime/hosts.rs +99 -0
  91. package/vendor/fastctx/src/runtime/journal.rs +556 -0
  92. package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
  93. package/vendor/fastctx/src/runtime/mod.rs +746 -0
  94. package/vendor/fastctx/src/runtime/protocol.rs +296 -0
  95. package/vendor/fastctx/src/runtime/session.rs +536 -0
  96. package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
  97. package/vendor/fastctx/src/search_parallelism.rs +106 -0
  98. package/vendor/fastctx/src/search_text.rs +227 -0
  99. package/vendor/fastctx/src/server.rs +359 -0
  100. package/vendor/fastctx/src/server_manifest.rs +468 -0
  101. package/vendor/fastctx/src/server_support.rs +826 -0
  102. package/vendor/fastctx/src/session.rs +629 -0
  103. package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
  104. package/vendor/fastctx/src/shell/bash.rs +263 -0
  105. package/vendor/fastctx/src/shell/buffer.rs +108 -0
  106. package/vendor/fastctx/src/shell/encoding.rs +403 -0
  107. package/vendor/fastctx/src/shell/foreground.rs +115 -0
  108. package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
  109. package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
  110. package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
  111. package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
  112. package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
  113. package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
  114. package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
  115. package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
  116. package/vendor/fastctx/src/shell/mod.rs +345 -0
  117. package/vendor/fastctx/src/shell/normalize.rs +389 -0
  118. package/vendor/fastctx/src/shell/output.rs +406 -0
  119. package/vendor/fastctx/src/shell/process.rs +493 -0
  120. package/vendor/fastctx/src/shell_server.rs +156 -0
  121. package/vendor/fastctx/src/skip_report.rs +83 -0
  122. package/vendor/fastctx/src/stdio_transport.rs +177 -0
  123. package/vendor/fastctx/src/tool_schema.rs +204 -0
  124. package/vendor/fastctx/src/traversal.rs +846 -0
  125. package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
  126. package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
  127. package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
  128. package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
  129. package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
  130. package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
  131. package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
  132. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
  133. package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
  134. package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
  135. package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
  136. package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
  137. package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
  138. package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
  139. package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
  140. 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
+ }