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,45 @@
|
|
|
1
|
+
// Modified by dsh-ops (https://github.com/T-Auto/dsh-ops): dropped the tui and update modules so the crate builds only the MCP server surface.
|
|
2
|
+
//! Local MCP toolkit for MCP hosts: default file tools plus an optional bash group.
|
|
3
|
+
|
|
4
|
+
pub(crate) mod background_status;
|
|
5
|
+
pub(crate) mod binary;
|
|
6
|
+
pub(crate) mod bounded_sort;
|
|
7
|
+
pub mod budget;
|
|
8
|
+
pub mod cli;
|
|
9
|
+
pub(crate) mod context_guard;
|
|
10
|
+
pub mod control;
|
|
11
|
+
pub mod edit;
|
|
12
|
+
mod edit_server;
|
|
13
|
+
pub mod encoding;
|
|
14
|
+
pub(crate) mod file_executor;
|
|
15
|
+
pub(crate) mod file_snapshot;
|
|
16
|
+
pub mod glob_filter;
|
|
17
|
+
pub mod glob_tool;
|
|
18
|
+
pub(crate) mod grep_sink;
|
|
19
|
+
pub mod grep_tool;
|
|
20
|
+
pub mod model;
|
|
21
|
+
pub(crate) mod model_guidance;
|
|
22
|
+
pub(crate) mod operation;
|
|
23
|
+
pub(crate) mod ordered_window;
|
|
24
|
+
pub(crate) mod os_environment;
|
|
25
|
+
pub(crate) mod path_codec;
|
|
26
|
+
pub mod paths;
|
|
27
|
+
pub(crate) mod process_identity;
|
|
28
|
+
pub(crate) mod process_policy;
|
|
29
|
+
pub mod read_tool;
|
|
30
|
+
pub(crate) mod render_plan;
|
|
31
|
+
pub(crate) mod runtime;
|
|
32
|
+
pub(crate) mod search_parallelism;
|
|
33
|
+
pub(crate) mod search_text;
|
|
34
|
+
pub mod server;
|
|
35
|
+
pub mod server_manifest;
|
|
36
|
+
pub(crate) mod server_support;
|
|
37
|
+
pub mod session;
|
|
38
|
+
pub mod shell;
|
|
39
|
+
mod shell_server;
|
|
40
|
+
pub(crate) mod skip_report;
|
|
41
|
+
pub(crate) mod stdio_transport;
|
|
42
|
+
pub(crate) mod tool_schema;
|
|
43
|
+
pub(crate) mod traversal;
|
|
44
|
+
|
|
45
|
+
pub use model::{ImageDetail, ToolContent, ToolResponse};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Modified by dsh-ops (https://github.com/T-Auto/dsh-ops): the crate no longer has a TUI mode, so this is only the MCP server and internal child-process entry point.
|
|
2
|
+
//! MCP server and internal child-process entry point for fastctx.
|
|
3
|
+
|
|
4
|
+
use std::process::ExitCode;
|
|
5
|
+
|
|
6
|
+
#[tokio::main(flavor = "current_thread")]
|
|
7
|
+
async fn main() -> ExitCode {
|
|
8
|
+
match fastctx::cli::run().await {
|
|
9
|
+
Ok(code) => code,
|
|
10
|
+
Err(error) => {
|
|
11
|
+
eprintln!("fastctx: {error}");
|
|
12
|
+
ExitCode::FAILURE
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//! Protocol-neutral content model between the tool core and MCP layer.
|
|
2
|
+
|
|
3
|
+
/// Image-detail hint for the Codex vision pipeline.
|
|
4
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
5
|
+
pub enum ImageDetail {
|
|
6
|
+
/// Request the high detail needed to preserve small PDF text.
|
|
7
|
+
High,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/// One content block returned by a tool.
|
|
11
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
12
|
+
pub enum ToolContent {
|
|
13
|
+
/// UTF-8 text for the model.
|
|
14
|
+
Text(String),
|
|
15
|
+
/// Base64 image data and MIME type; a missing detail uses the host default.
|
|
16
|
+
Image {
|
|
17
|
+
/// Standard Base64 data without a data-URL prefix.
|
|
18
|
+
data: String,
|
|
19
|
+
/// IANA MIME type corresponding to the magic bytes.
|
|
20
|
+
mime_type: String,
|
|
21
|
+
/// PDF page images request high detail; ordinary images defer to the host.
|
|
22
|
+
detail: Option<ImageDetail>,
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Observable tool response; errors remain MCP content and are marked by is_error.
|
|
27
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
28
|
+
pub struct ToolResponse {
|
|
29
|
+
/// Text or image blocks in model-consumption order.
|
|
30
|
+
pub content: Vec<ToolContent>,
|
|
31
|
+
/// When true, the MCP layer must return `isError: true`.
|
|
32
|
+
pub is_error: bool,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl ToolResponse {
|
|
36
|
+
/// Returns one successful text block.
|
|
37
|
+
pub fn text(text: impl Into<String>) -> Self {
|
|
38
|
+
Self {
|
|
39
|
+
content: vec![ToolContent::Text(text.into())],
|
|
40
|
+
is_error: false,
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Returns one text block with an explicit MCP error marker.
|
|
45
|
+
pub fn error(message: impl Into<String>) -> Self {
|
|
46
|
+
Self {
|
|
47
|
+
content: vec![ToolContent::Text(message.into())],
|
|
48
|
+
is_error: true,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//! Single-source prose for routing local-file work into the model-visible tool surface.
|
|
2
|
+
|
|
3
|
+
/// Positive routing shared by the host guidance file and every fresh MCP connection.
|
|
4
|
+
pub(crate) const LOCAL_FILE_ROUTE_GUIDANCE: &str = concat!(
|
|
5
|
+
"Use FastCtx file tools directly for local-file operations, including when a\n",
|
|
6
|
+
"local reference is URI-shaped; pass the equivalent plain absolute filesystem path."
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
/// Shared path-shape contract used by every file tool's path field.
|
|
10
|
+
pub(crate) const LOCAL_PATH_INPUT_GUIDANCE: &str = concat!(
|
|
11
|
+
"Plain absolute local filesystem path. When the source reference is URI-shaped, ",
|
|
12
|
+
"use its equivalent local absolute path."
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/// Opening sentence of the file-inspection tool's description.
|
|
16
|
+
///
|
|
17
|
+
/// It leads with what the tool is over a local filesystem path, because that is the
|
|
18
|
+
/// sentence a routing decision is made on: `read_mcp_resource` competes for the same
|
|
19
|
+
/// intent and the discriminating fact is the input shape, not the verb (2026-08-08).
|
|
20
|
+
const INSPECT_TOOL_SUMMARY: &str = concat!(
|
|
21
|
+
"Inspect the contents of local filesystem paths: one file (text, image, or PDF),\n",
|
|
22
|
+
"a batch of text files, or any file's raw bytes."
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const INSPECT_TOOL_DETAILS: &str = concat!(
|
|
26
|
+
"Text returns 1-based `N<tab>content` lines, as much of the file as\n",
|
|
27
|
+
"the output budget holds. For several text files in one call, pass\n",
|
|
28
|
+
"files=[{\"path\": ...}, ...] instead of file_path. Repeat a path for distinct\n",
|
|
29
|
+
"offset/limit ranges, and freely mix ranges from multiple files. A top-level limit\n",
|
|
30
|
+
"is the default for entries that omit one; an entry limit overrides it. The batch uses\n",
|
|
31
|
+
"one token budget,\n",
|
|
32
|
+
"per-entry problems reported inline, and a Partial note returns\n",
|
|
33
|
+
"the exact files array for the next call. Images (PNG/JPG/GIF/WebP/BMP) are\n",
|
|
34
|
+
"shown to you visually. PDFs return the selected pages' text layer or those\n",
|
|
35
|
+
"pages rendered as images; image mode defaults to 4 pages. view=\"hex\" dumps\n",
|
|
36
|
+
"any file's raw bytes. PDFs, images, and hex view are single-file only. Text\n",
|
|
37
|
+
"output is always UTF-8; when auto-detection is not confident it returns an\n",
|
|
38
|
+
"error listing candidate encodings instead of guessed text, so pass encoding\n",
|
|
39
|
+
"only then. Text, PDF, and hex responses end with a Complete or Partial status\n",
|
|
40
|
+
"— continue only with the exact parameters a Partial note provides."
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
pub(crate) fn local_path_description(context: &str) -> String {
|
|
44
|
+
format!("{LOCAL_PATH_INPUT_GUIDANCE} {context}")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub(crate) fn inspect_tool_description() -> String {
|
|
48
|
+
format!(
|
|
49
|
+
"{} {} {INSPECT_TOOL_DETAILS}",
|
|
50
|
+
INSPECT_TOOL_SUMMARY.replace('\n', " "),
|
|
51
|
+
LOCAL_FILE_ROUTE_GUIDANCE.replace('\n', " ")
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub(crate) fn server_instructions(enable_shell: bool) -> String {
|
|
56
|
+
let tools = if enable_shell {
|
|
57
|
+
"Local-file tools: inspect_local_file, grep, glob, replace, plus POSIX-bash shell tools."
|
|
58
|
+
} else {
|
|
59
|
+
"Local-file tools: inspect_local_file, grep, glob, and replace."
|
|
60
|
+
};
|
|
61
|
+
format!("{tools} {}", LOCAL_FILE_ROUTE_GUIDANCE.replace('\n', " "))
|
|
62
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
//! Request-scoped cancellation and retirement state for file operations.
|
|
2
|
+
|
|
3
|
+
use crate::model::ToolResponse;
|
|
4
|
+
use rmcp::model::RequestId;
|
|
5
|
+
use std::fmt;
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
8
|
+
use tokio_util::sync::CancellationToken;
|
|
9
|
+
|
|
10
|
+
struct CancelOnDropState {
|
|
11
|
+
cancel: CancellationToken,
|
|
12
|
+
armed: AtomicBool,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Cancels the blocking sibling whenever its async request future is dropped.
|
|
16
|
+
#[must_use]
|
|
17
|
+
pub(crate) struct RequestWorkGuard {
|
|
18
|
+
state: Arc<CancelOnDropState>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl RequestWorkGuard {
|
|
22
|
+
/// Creates a child cancellation scope shared with the blocking operation.
|
|
23
|
+
pub(crate) fn new(
|
|
24
|
+
request_id: RequestId,
|
|
25
|
+
request_cancel: CancellationToken,
|
|
26
|
+
) -> (Self, OperationCtx) {
|
|
27
|
+
Self::new_with_state(request_id, request_cancel, None)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fn new_with_state(
|
|
31
|
+
request_id: RequestId,
|
|
32
|
+
request_cancel: CancellationToken,
|
|
33
|
+
#[cfg(test)] stage_hook: Option<TestStageHook>,
|
|
34
|
+
#[cfg(not(test))] _stage_hook: Option<()>,
|
|
35
|
+
) -> (Self, OperationCtx) {
|
|
36
|
+
let state = Arc::new(CancelOnDropState {
|
|
37
|
+
cancel: request_cancel.child_token(),
|
|
38
|
+
armed: AtomicBool::new(true),
|
|
39
|
+
});
|
|
40
|
+
let operation = OperationCtx {
|
|
41
|
+
request_id,
|
|
42
|
+
state: Arc::clone(&state),
|
|
43
|
+
#[cfg(test)]
|
|
44
|
+
stage_hook,
|
|
45
|
+
};
|
|
46
|
+
(Self { state }, operation)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[cfg(test)]
|
|
50
|
+
pub(crate) fn new_with_hook(
|
|
51
|
+
request_id: RequestId,
|
|
52
|
+
request_cancel: CancellationToken,
|
|
53
|
+
stage_hook: TestStageHook,
|
|
54
|
+
) -> (Self, OperationCtx) {
|
|
55
|
+
Self::new_with_state(request_id, request_cancel, Some(stage_hook))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Marks a normally joined blocking sibling so dropping this guard is inert.
|
|
59
|
+
pub(crate) fn disarm(&mut self) {
|
|
60
|
+
self.state.armed.store(false, Ordering::Release);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl Drop for RequestWorkGuard {
|
|
65
|
+
fn drop(&mut self) {
|
|
66
|
+
if self.state.armed.swap(false, Ordering::AcqRel) {
|
|
67
|
+
self.state.cancel.cancel();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Request-wide cancellation state cloned into coordinators and worker contexts.
|
|
73
|
+
#[derive(Clone)]
|
|
74
|
+
pub(crate) struct OperationCtx {
|
|
75
|
+
request_id: RequestId,
|
|
76
|
+
state: Arc<CancelOnDropState>,
|
|
77
|
+
#[cfg(test)]
|
|
78
|
+
stage_hook: Option<TestStageHook>,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
impl OperationCtx {
|
|
82
|
+
/// Returns an error after the MCP request or its async wrapper is cancelled.
|
|
83
|
+
pub(crate) fn check(&self) -> Result<(), OpError> {
|
|
84
|
+
if self.state.cancel.is_cancelled() {
|
|
85
|
+
Err(OpError::Cancelled)
|
|
86
|
+
} else {
|
|
87
|
+
Ok(())
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Returns the token used by cancel-aware async admission.
|
|
92
|
+
pub(crate) fn cancellation_token(&self) -> &CancellationToken {
|
|
93
|
+
&self.state.cancel
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Creates request-only work state for coordinator and inline work.
|
|
97
|
+
pub(crate) fn inline_work(&self) -> WorkCtx {
|
|
98
|
+
WorkCtx {
|
|
99
|
+
request: self.clone(),
|
|
100
|
+
epoch: None,
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[cfg(test)]
|
|
105
|
+
pub(crate) fn stage(&self, stage: TestStage) {
|
|
106
|
+
if let Some(hook) = &self.stage_hook {
|
|
107
|
+
hook(stage);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
impl fmt::Debug for OperationCtx {
|
|
113
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
114
|
+
formatter
|
|
115
|
+
.debug_struct("OperationCtx")
|
|
116
|
+
.field("request_id", &self.request_id)
|
|
117
|
+
.field("cancelled", &self.state.cancel.is_cancelled())
|
|
118
|
+
.finish_non_exhaustive()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Generation-aware cancellation state for inline or speculative work.
|
|
123
|
+
#[derive(Clone, Debug)]
|
|
124
|
+
pub(crate) struct WorkCtx {
|
|
125
|
+
request: OperationCtx,
|
|
126
|
+
epoch: Option<EpochGuard>,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
impl WorkCtx {
|
|
130
|
+
/// Creates speculative work that must stop when `epoch` is retired.
|
|
131
|
+
pub(crate) fn speculative(request: OperationCtx, epoch: EpochGuard) -> Self {
|
|
132
|
+
Self {
|
|
133
|
+
request,
|
|
134
|
+
epoch: Some(epoch),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Checks request cancellation before speculative generation retirement.
|
|
139
|
+
pub(crate) fn check(&self) -> Result<(), WorkStop> {
|
|
140
|
+
if self.request.state.cancel.is_cancelled() {
|
|
141
|
+
return Err(WorkStop::RequestCancelled);
|
|
142
|
+
}
|
|
143
|
+
if let Some(epoch) = &self.epoch
|
|
144
|
+
&& epoch.current.load(Ordering::Acquire) != epoch.expected
|
|
145
|
+
{
|
|
146
|
+
return Err(WorkStop::EpochRetired);
|
|
147
|
+
}
|
|
148
|
+
Ok(())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Maps request-only work to the user-visible operation error channel.
|
|
152
|
+
pub(crate) fn check_inline(&self) -> Result<(), OpError> {
|
|
153
|
+
debug_assert!(self.epoch.is_none());
|
|
154
|
+
match self.check() {
|
|
155
|
+
Ok(()) => Ok(()),
|
|
156
|
+
Err(WorkStop::RequestCancelled) => Err(OpError::Cancelled),
|
|
157
|
+
Err(WorkStop::EpochRetired) => {
|
|
158
|
+
unreachable!("inline work never carries an epoch guard")
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[cfg(test)]
|
|
164
|
+
pub(crate) fn stage(&self, stage: TestStage) {
|
|
165
|
+
self.request.stage(stage);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Snapshot of the speculative generation a worker is allowed to publish into.
|
|
170
|
+
#[derive(Clone, Debug)]
|
|
171
|
+
pub(crate) struct EpochGuard {
|
|
172
|
+
expected: u64,
|
|
173
|
+
current: Arc<AtomicU64>,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
impl EpochGuard {
|
|
177
|
+
/// Captures one published generation for a speculative worker.
|
|
178
|
+
pub(crate) fn new(expected: u64, current: Arc<AtomicU64>) -> Self {
|
|
179
|
+
Self { expected, current }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Cooperative reasons a worker must stop without publishing an outcome.
|
|
184
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
185
|
+
pub(crate) enum WorkStop {
|
|
186
|
+
RequestCancelled,
|
|
187
|
+
EpochRetired,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// A cancellation/retirement checkpoint accepted by deep file-search loops.
|
|
191
|
+
pub(crate) trait WorkCheckpoint: Send + Sync {
|
|
192
|
+
/// Checks request cancellation before any speculative generation retirement.
|
|
193
|
+
fn check_work(&self) -> Result<(), WorkStop>;
|
|
194
|
+
|
|
195
|
+
#[cfg(test)]
|
|
196
|
+
fn stage(&self, stage: TestStage);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
impl WorkCheckpoint for OperationCtx {
|
|
200
|
+
fn check_work(&self) -> Result<(), WorkStop> {
|
|
201
|
+
self.check()
|
|
202
|
+
.map_err(|OpError::Cancelled| WorkStop::RequestCancelled)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[cfg(test)]
|
|
206
|
+
fn stage(&self, stage: TestStage) {
|
|
207
|
+
OperationCtx::stage(self, stage);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
impl WorkCheckpoint for WorkCtx {
|
|
212
|
+
fn check_work(&self) -> Result<(), WorkStop> {
|
|
213
|
+
self.check()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[cfg(test)]
|
|
217
|
+
fn stage(&self, stage: TestStage) {
|
|
218
|
+
WorkCtx::stage(self, stage);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Errors returned through the new grep/glob operation spine.
|
|
223
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
224
|
+
pub(crate) enum OpError {
|
|
225
|
+
Cancelled,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
impl OpError {
|
|
229
|
+
/// Converts operation control flow without disguising cancellation as success.
|
|
230
|
+
pub(crate) fn into_response(self) -> ToolResponse {
|
|
231
|
+
match self {
|
|
232
|
+
Self::Cancelled => ToolResponse::error("Request cancelled."),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#[cfg(test)]
|
|
238
|
+
pub(crate) type TestStageHook = Arc<dyn Fn(TestStage) + Send + Sync + 'static>;
|
|
239
|
+
|
|
240
|
+
/// Deterministic barriers and fault points used by cancellation ownership tests.
|
|
241
|
+
#[cfg(test)]
|
|
242
|
+
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
243
|
+
pub(crate) enum TestStage {
|
|
244
|
+
BeforeFilePermit,
|
|
245
|
+
AfterFilePermit,
|
|
246
|
+
RunnerQueued,
|
|
247
|
+
RunnerStarted,
|
|
248
|
+
BeforeBurstAcquire,
|
|
249
|
+
BeforeCandidateClaim,
|
|
250
|
+
AfterCandidateClaim,
|
|
251
|
+
BeforeReadyPublish,
|
|
252
|
+
TraversalEntry,
|
|
253
|
+
TraversalBatchFlush,
|
|
254
|
+
CapturePreflightRead,
|
|
255
|
+
SnapshotChunk,
|
|
256
|
+
SnapshotPromote,
|
|
257
|
+
BeforeIdentityPostCheck,
|
|
258
|
+
EncodingChunk,
|
|
259
|
+
LegacySegment,
|
|
260
|
+
CandidateValidation,
|
|
261
|
+
BeforeRegexSearch,
|
|
262
|
+
SinkMatch,
|
|
263
|
+
OccurrenceBatch,
|
|
264
|
+
OrderedReduce,
|
|
265
|
+
SortChunk,
|
|
266
|
+
SortMerge,
|
|
267
|
+
RenderUnit,
|
|
268
|
+
TokenProbe,
|
|
269
|
+
BeforeFinalTokenVerify,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[cfg(test)]
|
|
273
|
+
mod tests {
|
|
274
|
+
use super::{EpochGuard, OpError, RequestWorkGuard, TestStage, WorkCtx, WorkStop};
|
|
275
|
+
use rmcp::model::RequestId;
|
|
276
|
+
use std::collections::HashSet;
|
|
277
|
+
use std::sync::Arc;
|
|
278
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
279
|
+
use tokio_util::sync::CancellationToken;
|
|
280
|
+
|
|
281
|
+
fn request_id(value: i64) -> RequestId {
|
|
282
|
+
RequestId::Number(value)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[test]
|
|
286
|
+
fn dropping_an_armed_guard_cancels_only_its_child_scope() {
|
|
287
|
+
let parent = CancellationToken::new();
|
|
288
|
+
let (guard, operation) = RequestWorkGuard::new(request_id(1), parent.clone());
|
|
289
|
+
assert_eq!(operation.check(), Ok(()));
|
|
290
|
+
drop(guard);
|
|
291
|
+
assert_eq!(operation.check(), Err(OpError::Cancelled));
|
|
292
|
+
assert!(!parent.is_cancelled());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn disarming_a_joined_guard_does_not_cancel_the_operation_scope() {
|
|
297
|
+
let parent = CancellationToken::new();
|
|
298
|
+
let (mut guard, operation) = RequestWorkGuard::new(request_id(2), parent);
|
|
299
|
+
guard.disarm();
|
|
300
|
+
drop(guard);
|
|
301
|
+
assert_eq!(operation.check(), Ok(()));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
#[test]
|
|
305
|
+
fn work_checks_request_cancellation_before_epoch_retirement() {
|
|
306
|
+
let parent = CancellationToken::new();
|
|
307
|
+
let (_guard, operation) = RequestWorkGuard::new(request_id(3), parent.clone());
|
|
308
|
+
let generation = Arc::new(AtomicU64::new(7));
|
|
309
|
+
let work = WorkCtx {
|
|
310
|
+
request: operation,
|
|
311
|
+
epoch: Some(EpochGuard {
|
|
312
|
+
expected: 7,
|
|
313
|
+
current: Arc::clone(&generation),
|
|
314
|
+
}),
|
|
315
|
+
};
|
|
316
|
+
assert_eq!(work.check(), Ok(()));
|
|
317
|
+
generation.store(8, Ordering::Release);
|
|
318
|
+
assert_eq!(work.check(), Err(WorkStop::EpochRetired));
|
|
319
|
+
parent.cancel();
|
|
320
|
+
assert_eq!(work.check(), Err(WorkStop::RequestCancelled));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#[test]
|
|
324
|
+
fn stage_inventory_is_unique_and_complete() {
|
|
325
|
+
let stages = [
|
|
326
|
+
TestStage::BeforeFilePermit,
|
|
327
|
+
TestStage::AfterFilePermit,
|
|
328
|
+
TestStage::RunnerQueued,
|
|
329
|
+
TestStage::RunnerStarted,
|
|
330
|
+
TestStage::BeforeBurstAcquire,
|
|
331
|
+
TestStage::BeforeCandidateClaim,
|
|
332
|
+
TestStage::AfterCandidateClaim,
|
|
333
|
+
TestStage::BeforeReadyPublish,
|
|
334
|
+
TestStage::TraversalEntry,
|
|
335
|
+
TestStage::TraversalBatchFlush,
|
|
336
|
+
TestStage::CapturePreflightRead,
|
|
337
|
+
TestStage::SnapshotChunk,
|
|
338
|
+
TestStage::SnapshotPromote,
|
|
339
|
+
TestStage::BeforeIdentityPostCheck,
|
|
340
|
+
TestStage::EncodingChunk,
|
|
341
|
+
TestStage::LegacySegment,
|
|
342
|
+
TestStage::CandidateValidation,
|
|
343
|
+
TestStage::BeforeRegexSearch,
|
|
344
|
+
TestStage::SinkMatch,
|
|
345
|
+
TestStage::OccurrenceBatch,
|
|
346
|
+
TestStage::OrderedReduce,
|
|
347
|
+
TestStage::SortChunk,
|
|
348
|
+
TestStage::SortMerge,
|
|
349
|
+
TestStage::RenderUnit,
|
|
350
|
+
TestStage::TokenProbe,
|
|
351
|
+
TestStage::BeforeFinalTokenVerify,
|
|
352
|
+
];
|
|
353
|
+
assert_eq!(stages.len(), 26);
|
|
354
|
+
assert_eq!(stages.into_iter().collect::<HashSet<_>>().len(), 26);
|
|
355
|
+
}
|
|
356
|
+
}
|