hypershell 2.8.0__tar.gz → 2.8.2__tar.gz

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 (182) hide show
  1. {hypershell-2.8.0 → hypershell-2.8.2}/PKG-INFO +2 -2
  2. {hypershell-2.8.0 → hypershell-2.8.2}/README.rst +1 -1
  3. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_update_desc.rst +3 -1
  4. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_update_help.rst +3 -2
  5. {hypershell-2.8.0 → hypershell-2.8.2}/docs/index.rst +1 -1
  6. {hypershell-2.8.0 → hypershell-2.8.2}/docs/install.rst +19 -17
  7. {hypershell-2.8.0 → hypershell-2.8.2}/pyproject.toml +1 -1
  8. {hypershell-2.8.0 → hypershell-2.8.2}/share/man/man1/hs.1 +7 -4
  9. {hypershell-2.8.0 → hypershell-2.8.2}/share/man/man1/hsx.1 +7 -4
  10. {hypershell-2.8.0 → hypershell-2.8.2}/share/man/man1/hyper-shell.1 +7 -4
  11. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/config.py +1 -1
  12. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/exceptions.py +19 -1
  13. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/data/model.py +24 -5
  14. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/task.py +12 -4
  15. hypershell-2.8.2/tests/test_cancel.py +100 -0
  16. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_submit.py +22 -0
  17. {hypershell-2.8.0 → hypershell-2.8.2}/uv.lock +154 -129
  18. {hypershell-2.8.0 → hypershell-2.8.2}/.gitignore +0 -0
  19. {hypershell-2.8.0 → hypershell-2.8.2}/CODE_OF_CONDUCT.md +0 -0
  20. {hypershell-2.8.0 → hypershell-2.8.2}/LICENSE +0 -0
  21. {hypershell-2.8.0 → hypershell-2.8.2}/docs/Makefile +0 -0
  22. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/citation.rst +0 -0
  23. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/client_desc.rst +0 -0
  24. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/client_help.rst +0 -0
  25. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/client_usage.rst +0 -0
  26. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/cluster_desc.rst +0 -0
  27. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/cluster_help.rst +0 -0
  28. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/cluster_usage.rst +0 -0
  29. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_desc.rst +0 -0
  30. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_edit_desc.rst +0 -0
  31. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_edit_help.rst +0 -0
  32. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_edit_usage.rst +0 -0
  33. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_get_desc.rst +0 -0
  34. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_get_help.rst +0 -0
  35. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_get_usage.rst +0 -0
  36. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_help.rst +0 -0
  37. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_intro.rst +0 -0
  38. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_intro_alt.rst +0 -0
  39. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_param_ref.rst +0 -0
  40. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_pythonpath.rst +0 -0
  41. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_set_desc.rst +0 -0
  42. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_set_help.rst +0 -0
  43. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_set_usage.rst +0 -0
  44. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_site_vars.rst +0 -0
  45. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_task_env.rst +0 -0
  46. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_task_env_alt.rst +0 -0
  47. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_usage.rst +0 -0
  48. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_which_desc.rst +0 -0
  49. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_which_help.rst +0 -0
  50. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/config_which_usage.rst +0 -0
  51. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/database.rst +0 -0
  52. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/database_alt.rst +0 -0
  53. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/desc.rst +0 -0
  54. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/exit_status.rst +0 -0
  55. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/initdb_desc.rst +0 -0
  56. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/initdb_help.rst +0 -0
  57. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/initdb_usage.rst +0 -0
  58. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/license.rst +0 -0
  59. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/security_alt.rst +0 -0
  60. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/server_desc.rst +0 -0
  61. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/server_help.rst +0 -0
  62. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/server_usage.rst +0 -0
  63. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/signals.rst +0 -0
  64. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/submit_desc.rst +0 -0
  65. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/submit_help.rst +0 -0
  66. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/submit_usage.rst +0 -0
  67. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_groups_alt.rst +0 -0
  68. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_info_desc.rst +0 -0
  69. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_info_help.rst +0 -0
  70. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_info_usage.rst +0 -0
  71. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_run_desc.rst +0 -0
  72. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_run_help.rst +0 -0
  73. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_run_usage.rst +0 -0
  74. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_search_desc.rst +0 -0
  75. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_search_help.rst +0 -0
  76. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_search_usage.rst +0 -0
  77. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_submit_desc.rst +0 -0
  78. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_submit_help.rst +0 -0
  79. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_submit_usage.rst +0 -0
  80. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_update_usage.rst +0 -0
  81. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_wait_desc.rst +0 -0
  82. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_wait_help.rst +0 -0
  83. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/task_wait_usage.rst +0 -0
  84. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/templates.rst +0 -0
  85. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/templates_alt.rst +0 -0
  86. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_include/tls_cli_options.rst +0 -0
  87. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_static/custom.css +0 -0
  88. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_static/logo-dark-mode.png +0 -0
  89. {hypershell-2.8.0 → hypershell-2.8.2}/docs/_static/logo-light-mode.png +0 -0
  90. {hypershell-2.8.0 → hypershell-2.8.2}/docs/api/client.rst +0 -0
  91. {hypershell-2.8.0 → hypershell-2.8.2}/docs/api/cluster.rst +0 -0
  92. {hypershell-2.8.0 → hypershell-2.8.2}/docs/api/data.rst +0 -0
  93. {hypershell-2.8.0 → hypershell-2.8.2}/docs/api/index.rst +0 -0
  94. {hypershell-2.8.0 → hypershell-2.8.2}/docs/api/server.rst +0 -0
  95. {hypershell-2.8.0 → hypershell-2.8.2}/docs/api/submit.rst +0 -0
  96. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20230329_2_2_0_release.rst +0 -0
  97. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20230413_2_3_0_release.rst +0 -0
  98. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20230602_2_4_0_release.rst +0 -0
  99. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20240518_2_5_0_release.rst +0 -0
  100. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20240706_2_5_2_release.rst +0 -0
  101. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20241115_2_6_0_release.rst +0 -0
  102. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20241115_announce_logo.rst +0 -0
  103. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20241231_2_6_1_release.rst +0 -0
  104. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20250215_2_6_5_release.rst +0 -0
  105. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20250405_2_6_6_release.rst +0 -0
  106. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20250504_2_7_0_release.rst +0 -0
  107. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/20260705_2_8_0_release.rst +0 -0
  108. {hypershell-2.8.0 → hypershell-2.8.2}/docs/blog/index.rst +0 -0
  109. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/client.rst +0 -0
  110. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/cluster.rst +0 -0
  111. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/config.rst +0 -0
  112. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/config_edit.rst +0 -0
  113. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/config_get.rst +0 -0
  114. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/config_set.rst +0 -0
  115. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/config_which.rst +0 -0
  116. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/index.rst +0 -0
  117. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/initdb.rst +0 -0
  118. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/server.rst +0 -0
  119. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/submit.rst +0 -0
  120. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/task_info.rst +0 -0
  121. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/task_run.rst +0 -0
  122. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/task_search.rst +0 -0
  123. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/task_submit.rst +0 -0
  124. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/task_update.rst +0 -0
  125. {hypershell-2.8.0 → hypershell-2.8.2}/docs/cli/task_wait.rst +0 -0
  126. {hypershell-2.8.0 → hypershell-2.8.2}/docs/conf.py +0 -0
  127. {hypershell-2.8.0 → hypershell-2.8.2}/docs/config.rst +0 -0
  128. {hypershell-2.8.0 → hypershell-2.8.2}/docs/database.rst +0 -0
  129. {hypershell-2.8.0 → hypershell-2.8.2}/docs/getting_started.rst +0 -0
  130. {hypershell-2.8.0 → hypershell-2.8.2}/docs/logging.rst +0 -0
  131. {hypershell-2.8.0 → hypershell-2.8.2}/docs/manual.rst +0 -0
  132. {hypershell-2.8.0 → hypershell-2.8.2}/docs/roadmap.rst +0 -0
  133. {hypershell-2.8.0 → hypershell-2.8.2}/docs/security.rst +0 -0
  134. {hypershell-2.8.0 → hypershell-2.8.2}/docs/templates.rst +0 -0
  135. {hypershell-2.8.0 → hypershell-2.8.2}/docs/tutorial/advanced.rst +0 -0
  136. {hypershell-2.8.0 → hypershell-2.8.2}/docs/tutorial/basic.rst +0 -0
  137. {hypershell-2.8.0 → hypershell-2.8.2}/docs/tutorial/distributed.rst +0 -0
  138. {hypershell-2.8.0 → hypershell-2.8.2}/docs/tutorial/hybrid.rst +0 -0
  139. {hypershell-2.8.0 → hypershell-2.8.2}/share/bash_completion.d/hs +0 -0
  140. {hypershell-2.8.0 → hypershell-2.8.2}/share/bash_completion.d/hsx +0 -0
  141. {hypershell-2.8.0 → hypershell-2.8.2}/share/zsh/site-functions/_hs +0 -0
  142. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/__init__.py +0 -0
  143. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/client.py +0 -0
  144. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/cluster/__init__.py +0 -0
  145. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/cluster/local.py +0 -0
  146. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/cluster/remote.py +0 -0
  147. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/cluster/ssh.py +0 -0
  148. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/config.py +0 -0
  149. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/__init__.py +0 -0
  150. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/fsm.py +0 -0
  151. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/heartbeat.py +0 -0
  152. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/logging.py +0 -0
  153. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/platform.py +0 -0
  154. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/pretty_print.py +0 -0
  155. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/queue.py +0 -0
  156. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/remote.py +0 -0
  157. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/resource.py +0 -0
  158. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/signal.py +0 -0
  159. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/sys.py +0 -0
  160. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/tag.py +0 -0
  161. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/template.py +0 -0
  162. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/thread.py +0 -0
  163. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/tls.py +0 -0
  164. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/types.py +0 -0
  165. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/core/uuid.py +0 -0
  166. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/data/__init__.py +0 -0
  167. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/data/core.py +0 -0
  168. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/server.py +0 -0
  169. {hypershell-2.8.0 → hypershell-2.8.2}/src/hypershell/submit.py +0 -0
  170. {hypershell-2.8.0 → hypershell-2.8.2}/tests/__init__.py +0 -0
  171. {hypershell-2.8.0 → hypershell-2.8.2}/tests/conftest.py +0 -0
  172. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_autoscale.py +0 -0
  173. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_cluster.py +0 -0
  174. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_data/__init__.py +0 -0
  175. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_data/test_core.py +0 -0
  176. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_data/test_model.py +0 -0
  177. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_groups.py +0 -0
  178. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_initdb.py +0 -0
  179. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_logging.py +0 -0
  180. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_main.py +0 -0
  181. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_queue_tls.py +0 -0
  182. {hypershell-2.8.0 → hypershell-2.8.2}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypershell
3
- Version: 2.8.0
3
+ Version: 2.8.2
4
4
  Summary: A cross-platform, high-throughput computing utility for processing shell commands over a distributed, asynchronous queue.
5
5
  Project-URL: Homepage, https://hypershell.org
6
6
  Project-URL: Documentation, https://hypershell.readthedocs.io
@@ -54,7 +54,7 @@ HyperShell v2: Distributed Task Execution for HPC
54
54
  :target: https://github.com/hypershell/hypershell/releases
55
55
  :alt: Github Release
56
56
 
57
- .. image:: https://img.shields.io/badge/Python-3.9+-blue.svg
57
+ .. image:: https://img.shields.io/badge/Python-3.11+-blue.svg
58
58
  :target: https://www.python.org/downloads
59
59
  :alt: Python Versions
60
60
 
@@ -9,7 +9,7 @@ HyperShell v2: Distributed Task Execution for HPC
9
9
  :target: https://github.com/hypershell/hypershell/releases
10
10
  :alt: Github Release
11
11
 
12
- .. image:: https://img.shields.io/badge/Python-3.9+-blue.svg
12
+ .. image:: https://img.shields.io/badge/Python-3.11+-blue.svg
13
13
  :target: https://www.python.org/downloads
14
14
  :alt: Python Versions
15
15
 
@@ -4,7 +4,9 @@ Include any number of `FIELD=VALUE` or tag `KEY:VALUE` positional arguments.
4
4
 
5
5
  The ``-w``/``--where`` and ``-t``/``--with-tag`` operate just as in the search command.
6
6
 
7
- Using ``--cancel`` sets `schedule_time` to now and `exit_status` to -1.
7
+ Using ``--cancel`` marks tasks as cancelled: `schedule_time` and `completion_time`
8
+ are set to now and `exit_status` to -1. A cancelled task is terminal - it will not be
9
+ scheduled, retried, or reverted on ``--restart``.
8
10
 
9
11
  Using ``--revert`` reverts everything as if the task was new again.
10
12
 
@@ -10,8 +10,9 @@ Options
10
10
  ``--cancel``
11
11
  Cancel specified tasks.
12
12
 
13
- Cancelling a task means it will no longer be scheduled.
14
- This is done by setting the `schedule_time` to `now` and the `exit_status` to -1.
13
+ Cancelling a task means it will no longer be scheduled. This is done by setting the
14
+ `schedule_time` and `completion_time` to `now` and the `exit_status` to -1. The task
15
+ becomes terminal: it will not be scheduled, retried, or reverted on ``--restart``.
15
16
  A task cannot be cancelled after it is sent to remote clients.
16
17
 
17
18
  ``--revert``
@@ -11,7 +11,7 @@ Release v\ |release| (:ref:`Getting Started <getting_started>`)
11
11
  :target: https://github.com/hypershell/hypershell/releases
12
12
  :alt: Github Release
13
13
 
14
- .. image:: https://img.shields.io/badge/Python-3.9+-blue.svg
14
+ .. image:: https://img.shields.io/badge/Python-3.11+-blue.svg
15
15
  :target: https://www.python.org/downloads
16
16
  :alt: Python Versions
17
17
 
@@ -257,11 +257,9 @@ managed by some `module` system.
257
257
 
258
258
  Here we will create an isolated prefix for the installation with version number included
259
259
  and only expose the entry-point scripts to users, along with shell completions and the
260
- manual page. Some desired runtime, ``python3.13``, is already loaded.
261
-
262
- If there is not already a preferred Python module it might be easier to use
263
- `conda-forge <https://conda-forge.org>`_ (or `mamba <https://mamba.readthedocs.io>`_)
264
- or ``uv`` directly to create a virtual environment.
260
+ manual page. We can either use an existing Python runtime installed externally, otherwise
261
+ `conda-forge <https://conda-forge.org>`_ has the added benefit of complete isolation and
262
+ the ``psycopg-c`` dependencies for maximum security and performance.
265
263
 
266
264
  .. admonition:: Create installation manually on a shared system
267
265
  :class: note
@@ -271,15 +269,16 @@ or ``uv`` directly to create a virtual environment.
271
269
  mkdir -p /apps/x86_64-any/hypershell/$VERSION
272
270
  cd /apps/x86_64-any/hypershell/$VERSION
273
271
 
274
- git clone --depth=1 --branch=$VERSION https://github.com/hypershell/hypershell src
272
+ PKG='hypershell[postgres-system,uuid7,cron,zstd]'
273
+ URL='https://github.com/hypershell/hypershell'
275
274
 
276
- python3.13 -m venv libexec
277
- libexec/bin/pip install './src[postgres]'
275
+ python3.14 -m venv libexec
276
+ libexec/bin/pip install "$PKG @ git+$URL@$VERSION"
278
277
 
279
278
  mkdir -p bin
280
- ln -sf ../libexec/bin/hs bin/hs
281
- ln -sf ../libexec/bin/hsx bin/hsx
282
- ln -sf src/share
279
+ cd bin
280
+ ln -sf ../libexec/bin/hs
281
+ ln -sf ../libexec/bin/hsx
283
282
 
284
283
  |
285
284
 
@@ -303,10 +302,13 @@ configuration file might then be:
303
302
 
304
303
  prepend_path("PATH", pathJoin(modroot, "bin"))
305
304
  prepend_path("MANPATH", pathJoin(modroot, "share", "man"))
306
- prepend_path("FPATH", pathJoin(modroot, "share", "zsh", "site-functions")) -- Zsh completions
307
305
 
308
- -- Raw source b/c `complete -F _hs hs` does not persist with source_sh
309
- execute { cmd="source " .. pathJoin(modroot, "share", "bash_completion.d", "hs"), modeA={"load"} }
306
+ if myShellName() == "bash" then
307
+ source_sh("bash", pathJoin(modroot, "libexec", "share", "bash-completion", "completions", "hs"))
308
+ elseif myShellName() == "zsh" then
309
+ prepend_path("FPATH", pathJoin(modroot, "libexec", "share", "zsh", "site-functions"))
310
+ execute({ cmd = "autoload -Uz compinit && compinit", modeA = { "load" } })
311
+ end
310
312
 
311
313
  Presumably, users would then be able to activate the software by loading the module as such.
312
314
 
@@ -344,9 +346,9 @@ And we can include the following paths in our frozen set.
344
346
 
345
347
  .. code-block:: shell
346
348
 
347
- /apps/x86_64-any/hypershell/<version>/libexec/lib/python3.13
348
- /apps/x86_64-any/hypershell/<version>/libexec/lib/python3.13/lib-dynload
349
- /apps/x86_64-any/hypershell/<version>/libexec/lib/python3.13/site-packages
349
+ /apps/x86_64-any/hypershell/<version>/libexec/lib/python3.14
350
+ /apps/x86_64-any/hypershell/<version>/libexec/lib/python3.14/lib-dynload
351
+ /apps/x86_64-any/hypershell/<version>/libexec/lib/python3.14/site-packages
350
352
 
351
353
 
352
354
  ------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hypershell"
3
- version = "2.8.0"
3
+ version = "2.8.2"
4
4
  description = "A cross-platform, high-throughput computing utility for processing shell commands over a distributed, asynchronous queue."
5
5
  readme = "README.rst"
6
6
  license = "Apache-2.0"
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
28
28
  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
29
29
  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
30
30
  ..
31
- .TH "HS" "1" "Jul 05, 2026" "2.8.0" "hypershell"
31
+ .TH "HS" "1" "Jul 22, 2026" "2.8.2" "hypershell"
32
32
  .SH NAME
33
33
  hs \- Process shell commands over a distributed, asynchronous queue
34
34
  .SH SYNOPSIS
@@ -1360,7 +1360,9 @@ Include any number of \fIFIELD=VALUE\fP or tag \fIKEY:VALUE\fP positional argume
1360
1360
  .sp
1361
1361
  The \fB\-w\fP/\fB\-\-where\fP and \fB\-t\fP/\fB\-\-with\-tag\fP operate just as in the search command.
1362
1362
  .sp
1363
- Using \fB\-\-cancel\fP sets \fIschedule_time\fP to now and \fIexit_status\fP to \-1.
1363
+ Using \fB\-\-cancel\fP marks tasks as cancelled: \fIschedule_time\fP and \fIcompletion_time\fP
1364
+ are set to now and \fIexit_status\fP to \-1. A cancelled task is terminal \- it will not be
1365
+ scheduled, retried, or reverted on \fB\-\-restart\fP\&.
1364
1366
  .sp
1365
1367
  Using \fB\-\-revert\fP reverts everything as if the task was new again.
1366
1368
  .sp
@@ -1380,8 +1382,9 @@ Assignment pairs for update.
1380
1382
  .B \fB\-\-cancel\fP
1381
1383
  Cancel specified tasks.
1382
1384
  .sp
1383
- Cancelling a task means it will no longer be scheduled.
1384
- This is done by setting the \fIschedule_time\fP to \fInow\fP and the \fIexit_status\fP to \-1.
1385
+ Cancelling a task means it will no longer be scheduled. This is done by setting the
1386
+ \fIschedule_time\fP and \fIcompletion_time\fP to \fInow\fP and the \fIexit_status\fP to \-1. The task
1387
+ becomes terminal: it will not be scheduled, retried, or reverted on \fB\-\-restart\fP\&.
1385
1388
  A task cannot be cancelled after it is sent to remote clients.
1386
1389
  .TP
1387
1390
  .B \fB\-\-revert\fP
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
28
28
  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
29
29
  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
30
30
  ..
31
- .TH "HS" "1" "Jul 05, 2026" "2.8.0" "hypershell"
31
+ .TH "HS" "1" "Jul 22, 2026" "2.8.2" "hypershell"
32
32
  .SH NAME
33
33
  hs \- Process shell commands over a distributed, asynchronous queue
34
34
  .SH SYNOPSIS
@@ -1360,7 +1360,9 @@ Include any number of \fIFIELD=VALUE\fP or tag \fIKEY:VALUE\fP positional argume
1360
1360
  .sp
1361
1361
  The \fB\-w\fP/\fB\-\-where\fP and \fB\-t\fP/\fB\-\-with\-tag\fP operate just as in the search command.
1362
1362
  .sp
1363
- Using \fB\-\-cancel\fP sets \fIschedule_time\fP to now and \fIexit_status\fP to \-1.
1363
+ Using \fB\-\-cancel\fP marks tasks as cancelled: \fIschedule_time\fP and \fIcompletion_time\fP
1364
+ are set to now and \fIexit_status\fP to \-1. A cancelled task is terminal \- it will not be
1365
+ scheduled, retried, or reverted on \fB\-\-restart\fP\&.
1364
1366
  .sp
1365
1367
  Using \fB\-\-revert\fP reverts everything as if the task was new again.
1366
1368
  .sp
@@ -1380,8 +1382,9 @@ Assignment pairs for update.
1380
1382
  .B \fB\-\-cancel\fP
1381
1383
  Cancel specified tasks.
1382
1384
  .sp
1383
- Cancelling a task means it will no longer be scheduled.
1384
- This is done by setting the \fIschedule_time\fP to \fInow\fP and the \fIexit_status\fP to \-1.
1385
+ Cancelling a task means it will no longer be scheduled. This is done by setting the
1386
+ \fIschedule_time\fP and \fIcompletion_time\fP to \fInow\fP and the \fIexit_status\fP to \-1. The task
1387
+ becomes terminal: it will not be scheduled, retried, or reverted on \fB\-\-restart\fP\&.
1385
1388
  A task cannot be cancelled after it is sent to remote clients.
1386
1389
  .TP
1387
1390
  .B \fB\-\-revert\fP
@@ -28,7 +28,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
28
28
  .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
29
29
  .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
30
30
  ..
31
- .TH "HYPER-SHELL" "1" "Jul 05, 2026" "2.8.0" "hypershell"
31
+ .TH "HYPER-SHELL" "1" "Jul 22, 2026" "2.8.2" "hypershell"
32
32
  .SH NAME
33
33
  hyper-shell \- Process shell commands over a distributed, asynchronous queue
34
34
  .SH SYNOPSIS
@@ -1360,7 +1360,9 @@ Include any number of \fIFIELD=VALUE\fP or tag \fIKEY:VALUE\fP positional argume
1360
1360
  .sp
1361
1361
  The \fB\-w\fP/\fB\-\-where\fP and \fB\-t\fP/\fB\-\-with\-tag\fP operate just as in the search command.
1362
1362
  .sp
1363
- Using \fB\-\-cancel\fP sets \fIschedule_time\fP to now and \fIexit_status\fP to \-1.
1363
+ Using \fB\-\-cancel\fP marks tasks as cancelled: \fIschedule_time\fP and \fIcompletion_time\fP
1364
+ are set to now and \fIexit_status\fP to \-1. A cancelled task is terminal \- it will not be
1365
+ scheduled, retried, or reverted on \fB\-\-restart\fP\&.
1364
1366
  .sp
1365
1367
  Using \fB\-\-revert\fP reverts everything as if the task was new again.
1366
1368
  .sp
@@ -1380,8 +1382,9 @@ Assignment pairs for update.
1380
1382
  .B \fB\-\-cancel\fP
1381
1383
  Cancel specified tasks.
1382
1384
  .sp
1383
- Cancelling a task means it will no longer be scheduled.
1384
- This is done by setting the \fIschedule_time\fP to \fInow\fP and the \fIexit_status\fP to \-1.
1385
+ Cancelling a task means it will no longer be scheduled. This is done by setting the
1386
+ \fIschedule_time\fP and \fIcompletion_time\fP to \fInow\fP and the \fIexit_status\fP to \-1. The task
1387
+ becomes terminal: it will not be scheduled, retried, or reverted on \fB\-\-restart\fP\&.
1385
1388
  A task cannot be cancelled after it is sent to remote clients.
1386
1389
  .TP
1387
1390
  .B \fB\-\-revert\fP
@@ -225,7 +225,7 @@ def partial_load(system: Optional[str] = path.system.config,
225
225
  'default': default, **preload,
226
226
  'system': {} if not system else load_file(system),
227
227
  'user': {} if not user else load_file(user),
228
- 'local': {} if not user else load_file(local),
228
+ 'local': {} if not local else load_file(local),
229
229
  'env': load_env(),
230
230
  })
231
231
 
@@ -28,7 +28,7 @@ from hypershell.core.platform import default_path
28
28
  # Public interface
29
29
  __all__ = ['display_warning', 'display_error', 'display_critical', 'traceback_filepath', 'write_traceback',
30
30
  'handle_exception', 'handle_exception_silently', 'handle_disconnect', 'handle_address_unknown',
31
- 'HostAddressInfo', 'DatabaseUninitialized',
31
+ 'handle_broken_pipe', 'HostAddressInfo', 'DatabaseUninitialized',
32
32
  'get_shared_exception_mapping', ]
33
33
 
34
34
 
@@ -96,6 +96,24 @@ def handle_exception_silently(exc: Exception) -> int:
96
96
  return status
97
97
 
98
98
 
99
+ def handle_broken_pipe(exc: Exception) -> int: # noqa: unused exc
100
+ """Handle a downstream reader closing the pipe early (e.g. `hs list | head`).
101
+
102
+ Python replaces the POSIX ``SIGPIPE`` with a `BrokenPipeError` rather than terminating the
103
+ process, so we catch it here instead of dumping a traceback - the consumer simply stopped
104
+ reading, which is not an error. A bare exit would raise a *second* `BrokenPipeError` when the
105
+ interpreter flushes ``stdout`` at shutdown, so we redirect the ``stdout`` file descriptor to
106
+ the null device to discard that final flush. Cross-platform: no reliance on ``SIGPIPE``
107
+ (absent on Windows), and the process signal disposition is left untouched.
108
+ """
109
+ try:
110
+ devnull = os.open(os.devnull, os.O_WRONLY)
111
+ os.dup2(devnull, sys.stdout.fileno())
112
+ except (OSError, ValueError):
113
+ pass # stdout may lack a real file descriptor (already closed or replaced)
114
+ return exit_status.success
115
+
116
+
99
117
  def handle_address_unknown(exc: Exception, # noqa: unused
100
118
  logger: logging.Logger,
101
119
  status: int = exit_status.runtime_error) -> int:
@@ -25,7 +25,7 @@ from sqlalchemy.dialects.postgresql import SMALLINT, UUID as POSTGRES_UUID, JSON
25
25
  # Internal libs
26
26
  from hypershell.core.logging import Logger, HOSTNAME, INSTANCE
27
27
  from hypershell.core.heartbeat import Heartbeat
28
- from hypershell.core.types import JSONData, to_json_type, from_json_type, serialize, deserialize
28
+ from hypershell.core.types import JSONData, to_json_type, from_json_type, serialize, deserialize, parse_bytes
29
29
  from hypershell.core.uuid import uuid
30
30
  from hypershell.core.tag import Tag
31
31
  from hypershell.data.core import schema, Session
@@ -34,7 +34,7 @@ from hypershell.data.core import schema, Session
34
34
  __all__ = [
35
35
  'Task', 'Client', 'Entity', 'to_json_type', 'from_json_type', 'serialize_tasks', 'deserialize_tasks',
36
36
  'UUID', 'TEXT', 'INTEGER', 'SMALL_INTEGER', 'DATETIME', 'BOOLEAN', 'JSON',
37
- 'DEFAULT_TASK_GROUP', 'TaskGroupInfo',
37
+ 'DEFAULT_TASK_GROUP', 'CANCEL_STATUS', 'TaskGroupInfo',
38
38
  ]
39
39
 
40
40
  # Initialize logger
@@ -45,6 +45,15 @@ log = Logger.with_name(__name__)
45
45
  DEFAULT_TASK_GROUP: Final[int] = 0
46
46
  """Default task group for backwards compatibility."""
47
47
 
48
+ CANCEL_STATUS: Final[int] = -1
49
+ """Sentinel `exit_status` marking a cancelled task.
50
+
51
+ A cancelled task is terminal: it must never be scheduled, retried, or counted as an
52
+ unrecoverable failure. It is distinguished from a genuine failure (positive exit codes,
53
+ or the negative template/resource sentinels in `client.py`) solely by this value, so the
54
+ scheduler's failure and retry queries filter it out explicitly.
55
+ """
56
+
48
57
 
49
58
  class DatabaseError(Exception):
50
59
  """Generic database-related exception."""
@@ -335,7 +344,11 @@ class Task(Entity):
335
344
  tag = {**(tag or {}), **inline_tags, **{'part': 0, }}
336
345
  other['group'] = tag.pop('group', group)
337
346
  other['cores'] = tag.pop('cores', other.get('cores', None))
338
- other['memory'] = tag.pop('memory', other.get('memory', None))
347
+ # A memory value may arrive as a unit-bearing string ('2GB') from an inline
348
+ # `memory:` tag (parsed by smart_coerce, which leaves units alone) — parse it to
349
+ # integer bytes as the `--memory` flag does, so resource accounting stays numeric.
350
+ memory = tag.pop('memory', other.get('memory', None))
351
+ other['memory'] = parse_bytes(memory) if isinstance(memory, str) else memory
339
352
  other['timeout'] = tag.pop('timeout', other.get('timeout', None))
340
353
  return Task(id=uuid(), args=args,
341
354
  submit_id=INSTANCE, submit_host=HOSTNAME, submit_time=datetime.now().astimezone(),
@@ -437,6 +450,7 @@ class Task(Entity):
437
450
  .filter(Task.group == previous_group)
438
451
  .filter(cls.exit_status.isnot(None))
439
452
  .filter(cls.exit_status != 0)
453
+ .filter(cls.exit_status != CANCEL_STATUS) # NOTE: cancelled is terminal, not a failure
440
454
  .filter(cls.attempt >= attempts)
441
455
  .filter(cls.retried.is_(False))
442
456
  )
@@ -472,6 +486,7 @@ class Task(Entity):
472
486
  .order_by(cls.completion_time)
473
487
  .filter(cls.exit_status.isnot(None))
474
488
  .filter(cls.exit_status != 0)
489
+ .filter(cls.exit_status != CANCEL_STATUS) # NOTE: cancelled is terminal, not a failure
475
490
  .filter(cls.attempt < attempts)
476
491
  .filter(cls.retried.is_(False)))
477
492
  if group is not None:
@@ -605,11 +620,15 @@ class Task(Entity):
605
620
  @classmethod
606
621
  def cancel_all(cls: Type[Task], ids: List[str]) -> None:
607
622
  """Cancel all tasks identified by `ids`."""
623
+ # NOTE: completion_time is set so the task is terminal - otherwise it reads as
624
+ # "interrupted" (scheduled but incomplete) and gets reverted/re-run on restart.
625
+ now = datetime.now().astimezone()
608
626
  cls.update_all([
609
627
  {
610
628
  'id': id,
611
- 'schedule_time': datetime.now().astimezone(),
612
- 'exit_status': -1,
629
+ 'schedule_time': now,
630
+ 'completion_time': now,
631
+ 'exit_status': CANCEL_STATUS,
613
632
  }
614
633
  for id in ids
615
634
  ])
@@ -37,14 +37,15 @@ from sqlalchemy.sql.elements import BinaryExpression
37
37
  # Internal libs
38
38
  from hypershell.core.platform import default_path
39
39
  from hypershell.core.config import config
40
- from hypershell.core.exceptions import handle_exception, handle_exception_silently, get_shared_exception_mapping
40
+ from hypershell.core.exceptions import (handle_exception, handle_exception_silently, handle_broken_pipe,
41
+ get_shared_exception_mapping)
41
42
  from hypershell.core.logging import Logger, HOSTNAME
42
43
  from hypershell.core.remote import SSHConnection
43
44
  from hypershell.core.types import smart_coerce, JSONData, to_json_type
44
45
  from hypershell.core.pretty_print import format_tag, format_json, format_bytes
45
46
  from hypershell.core.tag import Tag
46
47
  from hypershell.data.core import Session
47
- from hypershell.data.model import Task, JSON
48
+ from hypershell.data.model import Task, JSON, CANCEL_STATUS
48
49
  from hypershell.data import ensuredb, DATABASE_DIALECT
49
50
 
50
51
  # Public interface
@@ -177,6 +178,7 @@ class TaskInfoApp(Application):
177
178
 
178
179
  exceptions = {
179
180
  Task.NotFound: functools.partial(handle_exception, logger=log, status=exit_status.runtime_error),
181
+ BrokenPipeError: handle_broken_pipe, # e.g. `hs info UUID --stdout | head`
180
182
  **get_shared_exception_mapping(__name__)
181
183
  }
182
184
 
@@ -641,6 +643,7 @@ class TaskSearchApp(Application, SearchableMixin):
641
643
 
642
644
  exceptions = {
643
645
  StatementError: functools.partial(handle_exception, logger=log, status=exit_status.runtime_error),
646
+ BrokenPipeError: handle_broken_pipe, # e.g. `hs list --all --csv | head`
644
647
  **get_shared_exception_mapping(__name__)
645
648
  }
646
649
 
@@ -820,7 +823,8 @@ UPDATE_HELP = f"""\
820
823
  Include any number of FIELD=VALUE or tag KEY:VALUE positional arguments.
821
824
  The -w/--where and -t/--with-tag operate just as in the search command.
822
825
 
823
- Using --cancel sets schedule_time to now and exit_status to {CANCEL_STATUS}.
826
+ Using --cancel marks tasks terminal (schedule_time and completion_time to now,
827
+ exit_status to {CANCEL_STATUS}); they will not be scheduled, retried, or reverted.
824
828
  Using --revert reverts everything as if the task was new again.
825
829
  Using --delete drops the row from the database entirely.
826
830
 
@@ -980,7 +984,11 @@ class TaskUpdateApp(Application, SearchableMixin):
980
984
  prev_count = query.filter(Task.schedule_time.isnot(None)).count()
981
985
  if prev_count > 0:
982
986
  log.warning(f'{prev_count} cancelled tasks already scheduled')
983
- field_updates['schedule_time'] = datetime.now().astimezone()
987
+ # NOTE: completion_time is set so the task is terminal - otherwise it reads as
988
+ # "interrupted" (scheduled but incomplete) and gets reverted/re-run on restart.
989
+ now = datetime.now().astimezone()
990
+ field_updates['schedule_time'] = now
991
+ field_updates['completion_time'] = now
984
992
  field_updates['exit_status'] = CANCEL_STATUS
985
993
 
986
994
  if self.revert_mode:
@@ -0,0 +1,100 @@
1
+ # SPDX-FileCopyrightText: 2026 Geoffrey Lentner
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Test task cancellation is terminal and never re-scheduled by the server."""
5
+
6
+
7
+ # Type annotations
8
+ from __future__ import annotations
9
+
10
+ # Standard libs
11
+ import re
12
+ from pathlib import Path
13
+
14
+ # External libs
15
+ from pytest import mark
16
+ from cmdkit.app import exit_status
17
+
18
+ # Internal libs
19
+ from tests import main, main_lines, create_taskfile
20
+
21
+
22
+ def submit_pending(temp_site: Path, count: int) -> None:
23
+ """Submit `count` uniquely-tagged echo tasks to the database as pending (not run)."""
24
+ tasks = [f'echo RUN_{n} # HYPERSHELL: n:{n}' for n in range(count)]
25
+ taskfile = create_taskfile(temp_site, tasks)
26
+ rc, _, _ = main(['hs', 'submit', str(taskfile)])
27
+ assert rc == exit_status.success
28
+
29
+
30
+ @mark.integration
31
+ def test_cancel_is_terminal(temp_site: Path) -> None:
32
+ """Cancelling a task marks it terminal: exit_status is the sentinel and completion_time is set.
33
+
34
+ A cancelled task that only had `schedule_time`/`exit_status` set (but no `completion_time`)
35
+ reads as "interrupted" to the scheduler and gets reverted/re-run - so completion_time
36
+ must be populated for the cancellation to stick.
37
+ """
38
+ submit_pending(temp_site, 3)
39
+
40
+ rc, _, _ = main(['hs', 'update', '--cancel', '-t', 'n:1', '--no-confirm'])
41
+ assert rc == exit_status.success
42
+
43
+ rc, out, _ = main_lines(['hs', 'list', 'exit_status', '-t', 'n:1'])
44
+ assert out == ['-1']
45
+
46
+ rc, out, _ = main_lines(['hs', 'list', 'completion_time', '-t', 'n:1'])
47
+ assert out not in (['null'], [''])
48
+
49
+
50
+ @mark.integration
51
+ def test_cancelled_task_not_reverted_on_restart(temp_site: Path) -> None:
52
+ """A cancelled task must not be reverted and re-run by `hs cluster --restart`.
53
+
54
+ Regression: `--cancel` left `completion_time` NULL, so `revert_interrupted()` on restart
55
+ treated the cancellation-set `schedule_time` as an interruption, reverted the task, and
56
+ re-ran it (flipping exit_status from the cancel sentinel to 0).
57
+ """
58
+ submit_pending(temp_site, 4)
59
+
60
+ rc, _, _ = main(['hs', 'update', '--cancel', '-t', 'n:2', '--no-confirm'])
61
+ assert rc == exit_status.success
62
+
63
+ rc, stdout, _ = main(['hs', 'cluster', '--restart'])
64
+ assert rc == exit_status.success
65
+
66
+ # The pending tasks run; the cancelled one does not.
67
+ assert set(re.findall(r'RUN_\d', stdout)) == {'RUN_0', 'RUN_1', 'RUN_3'}
68
+ assert 'RUN_2' not in stdout
69
+
70
+ # It remains cancelled, not completed successfully.
71
+ rc, out, _ = main_lines(['hs', 'list', 'exit_status', '-t', 'n:2'])
72
+ assert out == ['-1']
73
+
74
+
75
+ @mark.integration
76
+ def test_cancelled_task_not_retried(temp_site: Path) -> None:
77
+ """A cancelled task must not be picked up as a "failed" task for retry.
78
+
79
+ Regression: the retry query (`select_failed`) matched any `exit_status != 0`, so with
80
+ retries enabled the cancel sentinel (-1) looked like a failure and was re-scheduled.
81
+ """
82
+ submit_pending(temp_site, 3)
83
+
84
+ rc, _, _ = main(['hs', 'update', '--cancel', '-t', 'n:2', '--no-confirm'])
85
+ assert rc == exit_status.success
86
+
87
+ # File-mode cluster (not --restart, so no revert path) with retries enabled: once the
88
+ # pending tasks are exhausted the failed-task retry query runs - it must skip the cancelled task.
89
+ extra = create_taskfile(temp_site, ['echo EXTRA # HYPERSHELL: m:0'], filename='extra.in')
90
+ rc, stdout, _ = main(['hs', 'cluster', str(extra), '-r2'])
91
+ assert rc == exit_status.success
92
+
93
+ assert 'RUN_2' not in stdout
94
+
95
+ rc, out, _ = main_lines(['hs', 'list', 'exit_status', '-t', 'n:2'])
96
+ assert out == ['-1']
97
+
98
+ # No retry copy was created (a retry would add a second row with attempt=2).
99
+ rc, out, _ = main_lines(['hs', 'list', 'attempt', '-t', 'n:2'])
100
+ assert out == ['1']
@@ -18,6 +18,7 @@ from cmdkit.app import exit_status
18
18
  # Internal libs
19
19
  from tests import main, main_lines, NO_OUTPUT, create_taskfile_echo, assert_output, UUID_PATTERN
20
20
  from hypershell.submit import SubmitApp
21
+ from hypershell.data.model import Task
21
22
 
22
23
 
23
24
  @mark.integration
@@ -213,3 +214,24 @@ def test_submit_bundled(temp_site: Path) -> None:
213
214
  assert main_lines(['hs', 'list', 'args', '-f', 'plain', '-s', 'submit_time']) == (
214
215
  exit_status.success, [f'echo {n}' for n in range(100)], NO_OUTPUT
215
216
  )
217
+
218
+
219
+ @mark.unit
220
+ def test_inline_memory_tag_parses_units_to_bytes() -> None:
221
+ """An inline ``memory:<size>`` resource tag is parsed to integer bytes — like the ``--memory``
222
+ flag (parse_bytes) — not stored as a raw string.
223
+
224
+ smart_coerce (used for inline tag values) leaves unit-bearing strings like ``2GB`` untouched, so
225
+ without explicit parsing ``Task.memory`` would be a string and client-side resource accounting
226
+ (``memory_total < task.memory``, an int vs str comparison) crashes.
227
+ """
228
+ # The reported form: an inline HYPERSHELL comment carrying a unit suffix.
229
+ task = Task.new(args='echo AAA #HYPERSHELL: cores:1 memory:2GB timeout:10')
230
+ assert task.memory == 2147483648 # 2 * 1024**3
231
+ assert task.cores == 1 and task.timeout == 10
232
+ # The tag= kwarg form resolves identically.
233
+ assert Task.new(args='echo', tag={'memory': '512MB'}).memory == 536870912
234
+ # Unit-less, already-integer, and absent values are unaffected (no regression).
235
+ assert Task.new(args='echo', tag={'memory': '2000'}).memory == 2000
236
+ assert Task.new(args='echo', tag={'memory': 4096}).memory == 4096
237
+ assert Task.new(args='echo').memory is None