letify 2.6.3__tar.gz → 2.6.4__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 (210) hide show
  1. {letify-2.6.3 → letify-2.6.4}/PKG-INFO +1 -1
  2. {letify-2.6.3 → letify-2.6.4}/docs/SPEC.md +5 -1
  3. {letify-2.6.3 → letify-2.6.4}/letify/__init__.py +1 -1
  4. {letify-2.6.3 → letify-2.6.4}/letify/providers/modal.py +47 -4
  5. {letify-2.6.3 → letify-2.6.4}/letify-core/Cargo.lock +3 -3
  6. {letify-2.6.3 → letify-2.6.4}/letify-core/Cargo.toml +1 -1
  7. {letify-2.6.3 → letify-2.6.4}/letify-ext/package-lock.json +2 -2
  8. {letify-2.6.3 → letify-2.6.4}/letify-ext/package.json +1 -1
  9. {letify-2.6.3 → letify-2.6.4}/pyproject.toml +1 -1
  10. {letify-2.6.3 → letify-2.6.4}/tests/test_providers.py +32 -1
  11. {letify-2.6.3 → letify-2.6.4}/.github/workflows/ci.yml +0 -0
  12. {letify-2.6.3 → letify-2.6.4}/.github/workflows/docs.yml +0 -0
  13. {letify-2.6.3 → letify-2.6.4}/.github/workflows/publish.yml +0 -0
  14. {letify-2.6.3 → letify-2.6.4}/.gitignore +0 -0
  15. {letify-2.6.3 → letify-2.6.4}/.researchtree +0 -0
  16. {letify-2.6.3 → letify-2.6.4}/CLAUDE.md +0 -0
  17. {letify-2.6.3 → letify-2.6.4}/LICENSE +0 -0
  18. {letify-2.6.3 → letify-2.6.4}/PROJECT.md +0 -0
  19. {letify-2.6.3 → letify-2.6.4}/README.md +0 -0
  20. {letify-2.6.3 → letify-2.6.4}/docs/COMPONENT.md +0 -0
  21. {letify-2.6.3 → letify-2.6.4}/docs/INTENT.md +0 -0
  22. {letify-2.6.3 → letify-2.6.4}/docs/NETWORK.md +0 -0
  23. {letify-2.6.3 → letify-2.6.4}/docs/guide/01-getting-started.md +0 -0
  24. {letify-2.6.3 → letify-2.6.4}/docs/guide/02-providers.md +0 -0
  25. {letify-2.6.3 → letify-2.6.4}/docs/guide/03-execution-modes.md +0 -0
  26. {letify-2.6.3 → letify-2.6.4}/docs/guide/04-environments-and-data.md +0 -0
  27. {letify-2.6.3 → letify-2.6.4}/docs/guide/05-concurrency.md +0 -0
  28. {letify-2.6.3 → letify-2.6.4}/docs/guide/06-cost.md +0 -0
  29. {letify-2.6.3 → letify-2.6.4}/docs/guide/07-troubleshooting.md +0 -0
  30. {letify-2.6.3 → letify-2.6.4}/docs/guide/08-releasing.md +0 -0
  31. {letify-2.6.3 → letify-2.6.4}/docs/guide/README.md +0 -0
  32. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  33. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  34. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  35. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/04-/355/231/230/352/262/275/352/263/274-/353/215/260/354/235/264/355/204/260.md" +0 -0
  36. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  37. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  38. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  39. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  40. {letify-2.6.3 → letify-2.6.4}/docs/guide/ko/README.md +0 -0
  41. {letify-2.6.3 → letify-2.6.4}/docs/locales/README_ko.md +0 -0
  42. {letify-2.6.3 → letify-2.6.4}/docs/mkdocs.yml +0 -0
  43. {letify-2.6.3 → letify-2.6.4}/examples/README.md +0 -0
  44. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/.letify.example +0 -0
  45. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/00_smoke.py +0 -0
  46. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/01_sweep.py +0 -0
  47. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/02_resume.py +0 -0
  48. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/03_watch.py +0 -0
  49. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/README.md +0 -0
  50. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/README_ko.md +0 -0
  51. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/common.py +0 -0
  52. {letify-2.6.3 → letify-2.6.4}/examples/nvfp4-lora/recipe.py +0 -0
  53. {letify-2.6.3 → letify-2.6.4}/letify/cli.py +0 -0
  54. {letify-2.6.3 → letify-2.6.4}/letify/config/__init__.py +0 -0
  55. {letify-2.6.3 → letify-2.6.4}/letify/config/inventory.py +0 -0
  56. {letify-2.6.3 → letify-2.6.4}/letify/config/login.py +0 -0
  57. {letify-2.6.3 → letify-2.6.4}/letify/config/schema.py +0 -0
  58. {letify-2.6.3 → letify-2.6.4}/letify/config/secrets.py +0 -0
  59. {letify-2.6.3 → letify-2.6.4}/letify/config/writer.py +0 -0
  60. {letify-2.6.3 → letify-2.6.4}/letify/declare/__init__.py +0 -0
  61. {letify-2.6.3 → letify-2.6.4}/letify/declare/cache.py +0 -0
  62. {letify-2.6.3 → letify-2.6.4}/letify/declare/env.py +0 -0
  63. {letify-2.6.3 → letify-2.6.4}/letify/declare/function.py +0 -0
  64. {letify-2.6.3 → letify-2.6.4}/letify/declare/instance.py +0 -0
  65. {letify-2.6.3 → letify-2.6.4}/letify/declare/reduce.py +0 -0
  66. {letify-2.6.3 → letify-2.6.4}/letify/declare/replicas.py +0 -0
  67. {letify-2.6.3 → letify-2.6.4}/letify/errors.py +0 -0
  68. {letify-2.6.3 → letify-2.6.4}/letify/install.py +0 -0
  69. {letify-2.6.3 → letify-2.6.4}/letify/launcher.py +0 -0
  70. {letify-2.6.3 → letify-2.6.4}/letify/paths.py +0 -0
  71. {letify-2.6.3 → letify-2.6.4}/letify/protocol/__init__.py +0 -0
  72. {letify-2.6.3 → letify-2.6.4}/letify/protocol/codec.py +0 -0
  73. {letify-2.6.3 → letify-2.6.4}/letify/protocol/driver.py +0 -0
  74. {letify-2.6.3 → letify-2.6.4}/letify/protocol/handle.py +0 -0
  75. {letify-2.6.3 → letify-2.6.4}/letify/protocol/wire.py +0 -0
  76. {letify-2.6.3 → letify-2.6.4}/letify/protocol/worker.py +0 -0
  77. {letify-2.6.3 → letify-2.6.4}/letify/providers/__init__.py +0 -0
  78. {letify-2.6.3 → letify-2.6.4}/letify/providers/base.py +0 -0
  79. {letify-2.6.3 → letify-2.6.4}/letify/providers/colab.py +0 -0
  80. {letify-2.6.3 → letify-2.6.4}/letify/providers/colab_files.py +0 -0
  81. {letify-2.6.3 → letify-2.6.4}/letify/providers/colab_keepalive.py +0 -0
  82. {letify-2.6.3 → letify-2.6.4}/letify/providers/elice.py +0 -0
  83. {letify-2.6.3 → letify-2.6.4}/letify/providers/kaggle.py +0 -0
  84. {letify-2.6.3 → letify-2.6.4}/letify/providers/kaggle_adapter.py +0 -0
  85. {letify-2.6.3 → letify-2.6.4}/letify/providers/local.py +0 -0
  86. {letify-2.6.3 → letify-2.6.4}/letify/providers/modal_adapter.py +0 -0
  87. {letify-2.6.3 → letify-2.6.4}/letify/providers/naming.py +0 -0
  88. {letify-2.6.3 → letify-2.6.4}/letify/providers/shell.py +0 -0
  89. {letify-2.6.3 → letify-2.6.4}/letify/providers/tunnel.py +0 -0
  90. {letify-2.6.3 → letify-2.6.4}/letify/providers/usage.py +0 -0
  91. {letify-2.6.3 → letify-2.6.4}/letify/remoting/__init__.py +0 -0
  92. {letify-2.6.3 → letify-2.6.4}/letify/remoting/capability.py +0 -0
  93. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/__init__.py +0 -0
  94. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/client.py +0 -0
  95. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/cuda.py +0 -0
  96. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/executor.py +0 -0
  97. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/frames.py +0 -0
  98. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/guard.py +0 -0
  99. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/tensor.py +0 -0
  100. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/trace.py +0 -0
  101. {letify-2.6.3 → letify-2.6.4}/letify/remoting/device/value.py +0 -0
  102. {letify-2.6.3 → letify-2.6.4}/letify/remoting/loader.py +0 -0
  103. {letify-2.6.3 → letify-2.6.4}/letify/remoting/probe.py +0 -0
  104. {letify-2.6.3 → letify-2.6.4}/letify/render.py +0 -0
  105. {letify-2.6.3 → letify-2.6.4}/letify/runtime/__init__.py +0 -0
  106. {letify-2.6.3 → letify-2.6.4}/letify/runtime/bootstrap.py +0 -0
  107. {letify-2.6.3 → letify-2.6.4}/letify/runtime/broker.py +0 -0
  108. {letify-2.6.3 → letify-2.6.4}/letify/runtime/channel.py +0 -0
  109. {letify-2.6.3 → letify-2.6.4}/letify/runtime/lease.py +0 -0
  110. {letify-2.6.3 → letify-2.6.4}/letify/runtime/pool.py +0 -0
  111. {letify-2.6.3 → letify-2.6.4}/letify/runtime/session.py +0 -0
  112. {letify-2.6.3 → letify-2.6.4}/letify/runtime/telemetry.py +0 -0
  113. {letify-2.6.3 → letify-2.6.4}/letify/store/__init__.py +0 -0
  114. {letify-2.6.3 → letify-2.6.4}/letify/store/backends/__init__.py +0 -0
  115. {letify-2.6.3 → letify-2.6.4}/letify/store/backends/filesystem.py +0 -0
  116. {letify-2.6.3 → letify-2.6.4}/letify/store/backends/google_auth.py +0 -0
  117. {letify-2.6.3 → letify-2.6.4}/letify/store/backends/layout.py +0 -0
  118. {letify-2.6.3 → letify-2.6.4}/letify/store/backends/objects.py +0 -0
  119. {letify-2.6.3 → letify-2.6.4}/letify/store/cas.py +0 -0
  120. {letify-2.6.3 → letify-2.6.4}/letify/store/pathdata.py +0 -0
  121. {letify-2.6.3 → letify-2.6.4}/letify/store/sendorder.py +0 -0
  122. {letify-2.6.3 → letify-2.6.4}/letify/store/volume.py +0 -0
  123. {letify-2.6.3 → letify-2.6.4}/letify/stubs.py +0 -0
  124. {letify-2.6.3 → letify-2.6.4}/letify/tools.py +0 -0
  125. {letify-2.6.3 → letify-2.6.4}/letify/transport/__init__.py +0 -0
  126. {letify-2.6.3 → letify-2.6.4}/letify/transport/agent.py +0 -0
  127. {letify-2.6.3 → letify-2.6.4}/letify/transport/announce.py +0 -0
  128. {letify-2.6.3 → letify-2.6.4}/letify/transport/link.py +0 -0
  129. {letify-2.6.3 → letify-2.6.4}/letify/transport/nat.py +0 -0
  130. {letify-2.6.3 → letify-2.6.4}/letify/transport/pipeline.py +0 -0
  131. {letify-2.6.3 → letify-2.6.4}/letify/transport/portmap.py +0 -0
  132. {letify-2.6.3 → letify-2.6.4}/letify/transport/probe.py +0 -0
  133. {letify-2.6.3 → letify-2.6.4}/letify/transport/quic.py +0 -0
  134. {letify-2.6.3 → letify-2.6.4}/letify/transport/rendezvous.py +0 -0
  135. {letify-2.6.3 → letify-2.6.4}/letify/transport/setup.py +0 -0
  136. {letify-2.6.3 → letify-2.6.4}/letify/transport/sshopts.py +0 -0
  137. {letify-2.6.3 → letify-2.6.4}/letify/transport/strategies.py +0 -0
  138. {letify-2.6.3 → letify-2.6.4}/letify-core/.gitignore +0 -0
  139. {letify-2.6.3 → letify-2.6.4}/letify-core/build.py +0 -0
  140. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-agent/Cargo.toml +0 -0
  141. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-agent/src/driver.rs +0 -0
  142. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-agent/src/main.rs +0 -0
  143. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-driver/Cargo.toml +0 -0
  144. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-driver/src/client.rs +0 -0
  145. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-driver/src/exports.rs +0 -0
  146. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-driver/src/image.rs +0 -0
  147. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-driver/src/lib.rs +0 -0
  148. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-driver/src/status.rs +0 -0
  149. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-driver/src/table.rs +0 -0
  150. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-quic/Cargo.toml +0 -0
  151. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-quic/src/main.rs +0 -0
  152. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-wire/Cargo.toml +0 -0
  153. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-wire/src/lib.rs +0 -0
  154. {letify-2.6.3 → letify-2.6.4}/letify-core/letify-wire/tests/throughput.rs +0 -0
  155. {letify-2.6.3 → letify-2.6.4}/letify-core/rust-toolchain.toml +0 -0
  156. {letify-2.6.3 → letify-2.6.4}/letify-ext/.vscodeignore +0 -0
  157. {letify-2.6.3 → letify-2.6.4}/letify-ext/LICENSE +0 -0
  158. {letify-2.6.3 → letify-2.6.4}/letify-ext/README.md +0 -0
  159. {letify-2.6.3 → letify-2.6.4}/letify-ext/media/icon.svg +0 -0
  160. {letify-2.6.3 → letify-2.6.4}/letify-ext/src/cli.ts +0 -0
  161. {letify-2.6.3 → letify-2.6.4}/letify-ext/src/extension.ts +0 -0
  162. {letify-2.6.3 → letify-2.6.4}/letify-ext/src/history.ts +0 -0
  163. {letify-2.6.3 → letify-2.6.4}/letify-ext/src/model.ts +0 -0
  164. {letify-2.6.3 → letify-2.6.4}/letify-ext/src/poll.ts +0 -0
  165. {letify-2.6.3 → letify-2.6.4}/letify-ext/src/render.ts +0 -0
  166. {letify-2.6.3 → letify-2.6.4}/letify-ext/test/cli.test.ts +0 -0
  167. {letify-2.6.3 → letify-2.6.4}/letify-ext/test/manifest.test.ts +0 -0
  168. {letify-2.6.3 → letify-2.6.4}/letify-ext/test/model.test.ts +0 -0
  169. {letify-2.6.3 → letify-2.6.4}/letify-ext/test/regressions.test.ts +0 -0
  170. {letify-2.6.3 → letify-2.6.4}/letify-ext/tsconfig.json +0 -0
  171. {letify-2.6.3 → letify-2.6.4}/letify_providers.py +0 -0
  172. {letify-2.6.3 → letify-2.6.4}/scripts/sync-main.sh +0 -0
  173. {letify-2.6.3 → letify-2.6.4}/scripts/version.py +0 -0
  174. {letify-2.6.3 → letify-2.6.4}/tests/conftest.py +0 -0
  175. {letify-2.6.3 → letify-2.6.4}/tests/fake_kaggle_adapter.py +0 -0
  176. {letify-2.6.3 → letify-2.6.4}/tests/fake_modal_adapter.py +0 -0
  177. {letify-2.6.3 → letify-2.6.4}/tests/test_cli.py +0 -0
  178. {letify-2.6.3 → letify-2.6.4}/tests/test_cli_output.py +0 -0
  179. {letify-2.6.3 → letify-2.6.4}/tests/test_colab_keepalive.py +0 -0
  180. {letify-2.6.3 → letify-2.6.4}/tests/test_config.py +0 -0
  181. {letify-2.6.3 → letify-2.6.4}/tests/test_core.py +0 -0
  182. {letify-2.6.3 → letify-2.6.4}/tests/test_data.py +0 -0
  183. {letify-2.6.3 → letify-2.6.4}/tests/test_data_streaming.py +0 -0
  184. {letify-2.6.3 → letify-2.6.4}/tests/test_declare.py +0 -0
  185. {letify-2.6.3 → letify-2.6.4}/tests/test_device.py +0 -0
  186. {letify-2.6.3 → letify-2.6.4}/tests/test_distributed.py +0 -0
  187. {letify-2.6.3 → letify-2.6.4}/tests/test_editor_sessions.py +0 -0
  188. {letify-2.6.3 → letify-2.6.4}/tests/test_elice.py +0 -0
  189. {letify-2.6.3 → letify-2.6.4}/tests/test_frames.py +0 -0
  190. {letify-2.6.3 → letify-2.6.4}/tests/test_install.py +0 -0
  191. {letify-2.6.3 → letify-2.6.4}/tests/test_kaggle.py +0 -0
  192. {letify-2.6.3 → letify-2.6.4}/tests/test_kaggle_cookie.py +0 -0
  193. {letify-2.6.3 → letify-2.6.4}/tests/test_kaggle_link.py +0 -0
  194. {letify-2.6.3 → letify-2.6.4}/tests/test_link_log.py +0 -0
  195. {letify-2.6.3 → letify-2.6.4}/tests/test_notebook.py +0 -0
  196. {letify-2.6.3 → letify-2.6.4}/tests/test_parallel_placement.py +0 -0
  197. {letify-2.6.3 → letify-2.6.4}/tests/test_paths.py +0 -0
  198. {letify-2.6.3 → letify-2.6.4}/tests/test_portmap.py +0 -0
  199. {letify-2.6.3 → letify-2.6.4}/tests/test_protocol.py +0 -0
  200. {letify-2.6.3 → letify-2.6.4}/tests/test_quic.py +0 -0
  201. {letify-2.6.3 → letify-2.6.4}/tests/test_remoting.py +0 -0
  202. {letify-2.6.3 → letify-2.6.4}/tests/test_render.py +0 -0
  203. {letify-2.6.3 → letify-2.6.4}/tests/test_runtime.py +0 -0
  204. {letify-2.6.3 → letify-2.6.4}/tests/test_ship.py +0 -0
  205. {letify-2.6.3 → letify-2.6.4}/tests/test_store.py +0 -0
  206. {letify-2.6.3 → letify-2.6.4}/tests/test_strategies.py +0 -0
  207. {letify-2.6.3 → letify-2.6.4}/tests/test_stubs.py +0 -0
  208. {letify-2.6.3 → letify-2.6.4}/tests/test_transport.py +0 -0
  209. {letify-2.6.3 → letify-2.6.4}/tests/test_tunnel_setup.py +0 -0
  210. {letify-2.6.3 → letify-2.6.4}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.6.3
3
+ Version: 2.6.4
4
4
  Summary: Declarations that become infrastructure.
5
5
  Project-URL: Homepage, https://github.com/DarkPyonix/letify
6
6
  Project-URL: Documentation, https://github.com/DarkPyonix/letify/tree/main/docs
@@ -807,7 +807,11 @@ A `read_until` blocks until the sandbox prints a line, and the adapter answers o
807
807
  - `Modal.stop` calls `abort()` when the adapter is out of step or its lock is still held after 5 s. The call timeout watchdog calls it through `SandboxChannel._kill`, so a call past its `timeout` ends its sandbox too.
808
808
  - The adapter's `terminate` for an id it did not create finds the sandbox with `modal.Sandbox.from_id` and terminates it. A sandbox that is already gone is not an error.
809
809
 
810
- Modal also bounds a sandbox that nothing terminates. `create` passes `timeout`, the entry option `timeout`, 3600 s by default, as the sandbox's maximum lifetime. It passes `idle_timeout`, the entry option `idle_timeout`, 600 s by default, after which Modal terminates a sandbox that is idle.
810
+ Modal also bounds a sandbox that nothing terminates. `create` passes `idle_timeout`, the entry option `idle_timeout`, 600 s by default, after which Modal terminates a sandbox that is idle. That is the bound that stops an abandoned sandbox billing, because an abandoned one is idle by definition once its worker has no one to answer.
811
+
812
+ It passes `timeout`, the entry option `timeout`, as the sandbox's maximum lifetime, and the default is the longest Modal accepts, 86400 s, measured as accepted. It is not a shorter number letify picked, for the reason The declaration gives for `timeout` having no default: a deadline letify invented would end a two hour training run at whatever hour it guessed. The default was 3600 s, exactly one hour, and it ended two Modal sessions of a real training run at one hour each with nothing saying why, which is that sentence happening. An account that wants a hard cap sets `timeout` itself.
813
+
814
+ A sandbox that reaches that lifetime takes its worker with it, so the channel sees a worker that stopped without replying. Where the session's age is within a minute of the lifetime letify asked for, the failure says so instead of listing causes: `the Modal sandbox reached the <seconds> s lifetime letify asked for; raise timeout on the account to run longer`.
811
815
 
812
816
  ### Modal data channel <!-- id: modal-data-channel -->
813
817
 
@@ -66,7 +66,7 @@ distributed: Final = Host.distributed
66
66
  # The enum class stays importable from letify.declare.instance for letify's own use.
67
67
  del Host
68
68
 
69
- __version__ = "2.6.3"
69
+ __version__ = "2.6.4"
70
70
 
71
71
 
72
72
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -102,6 +102,26 @@ STOP_WAIT = 5.0
102
102
  #: ``idle_timeout``.
103
103
  IDLE_TIMEOUT = 600
104
104
 
105
+ #: The sandbox's maximum lifetime, the longest Modal accepts, measured as accepted.
106
+ #: Spec "Modal": not a shorter number letify picked, for the reason The declaration gives
107
+ #: for ``timeout`` having no default. The previous 3600 s ended two sessions of a real
108
+ #: training run at one hour each with nothing saying why.
109
+ SANDBOX_TIMEOUT = 86400
110
+
111
+
112
+ def lifetime_detail(*, age_s: float, lifetime_s: int) -> str | None:
113
+ """What to say when a sandbox died at the lifetime letify asked for, or None.
114
+
115
+ Spec "Modal": the channel otherwise reports a worker that stopped without replying and
116
+ lists causes, none of which is the deadline letify set itself.
117
+ """
118
+ if lifetime_s <= 0 or age_s < lifetime_s - 60.0:
119
+ return None
120
+ return (
121
+ f"the Modal sandbox reached the {lifetime_s} s lifetime letify asked for; "
122
+ f"raise timeout on the account to run longer"
123
+ )
124
+
105
125
 
106
126
  class VolumePathMissing(RuntimeFailure):
107
127
  """The adapter answered that a volume path does not exist."""
@@ -377,6 +397,18 @@ class Modal(Provider):
377
397
  self._sandboxes: dict[str, str] = {}
378
398
  self._adapter: Adapter | None = None
379
399
 
400
+ @property
401
+ def sandbox_timeout_s(self) -> int:
402
+ """The sandbox's maximum lifetime, from the account or the longest Modal accepts."""
403
+ value = self.config.option("timeout", SANDBOX_TIMEOUT)
404
+ return max(1, int(value))
405
+
406
+ @property
407
+ def sandbox_idle_timeout_s(self) -> int:
408
+ """Seconds a sandbox may be idle before Modal terminates it. Spec "Modal"."""
409
+ value = self.config.option("idle_timeout", IDLE_TIMEOUT)
410
+ return max(1, int(value))
411
+
380
412
  def adapter(self) -> Adapter:
381
413
  """The adapter acting as this account, started on its first request."""
382
414
  if self._adapter is None or self._adapter.closed:
@@ -440,14 +472,15 @@ class Modal(Provider):
440
472
  args=["python3", "-u", "-c", BOOTSTRAP],
441
473
  packages=list(WORKER_PACKAGES),
442
474
  gpu=self.wire_name(runtime.instance) or None,
443
- timeout=int(self.config.option("timeout", 3600)),
444
- idle_timeout=int(self.config.option("idle_timeout", IDLE_TIMEOUT)),
475
+ timeout=self.sandbox_timeout_s,
476
+ idle_timeout=self.sandbox_idle_timeout_s,
445
477
  ports=[] if data_port is None else [data_port],
446
478
  )
447
479
  sandbox = str(created["sandbox"])
448
480
  self._sandboxes[runtime.name] = sandbox
449
481
  return SandboxChannel(
450
- adapter, sandbox, name=runtime.name, data_port=data_port, streams=streams
482
+ adapter, sandbox, name=runtime.name, data_port=data_port, streams=streams,
483
+ lifetime_s=self.sandbox_timeout_s,
451
484
  )
452
485
 
453
486
  def stop(self, runtime: Runtime) -> None:
@@ -598,11 +631,15 @@ class SandboxChannel(FramedChannel):
598
631
  name: str,
599
632
  data_port: int | None = None,
600
633
  streams: int = 1,
634
+ lifetime_s: int = 0,
601
635
  ):
602
636
  self.adapter = adapter
603
637
  self.sandbox = sandbox
604
638
  self.name = name
605
639
  self.data_port = data_port
640
+ #: The sandbox's maximum lifetime, so a death at it is named rather than guessed.
641
+ self.lifetime_s = lifetime_s
642
+ self.opened_at = time.monotonic()
606
643
  #: TCP connections the data channel carries its frames over.
607
644
  self.streams = streams
608
645
  self._sockets: list[socket.socket] = []
@@ -628,7 +665,13 @@ class SandboxChannel(FramedChannel):
628
665
  self._await_ready()
629
666
 
630
667
  def _raw_text(self) -> str:
631
- return bytes(self._raw[-2000:]).decode("utf-8", "replace")
668
+ text = bytes(self._raw[-2000:]).decode("utf-8", "replace")
669
+ # Spec "Modal": a sandbox that reached the lifetime letify asked for takes its
670
+ # worker with it, and the channel otherwise lists causes, none of them this one.
671
+ said = lifetime_detail(
672
+ age_s=time.monotonic() - self.opened_at, lifetime_s=self.lifetime_s
673
+ )
674
+ return f"{said}\n{text}" if said else text
632
675
 
633
676
  def _send_worker(self) -> None:
634
677
  # After a reexec the data connection is gone, so the source goes back over stdio.
@@ -138,7 +138,7 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify-agent"
141
- version = "2.6.3"
141
+ version = "2.6.4"
142
142
  dependencies = [
143
143
  "letify-wire",
144
144
  "libloading",
@@ -146,7 +146,7 @@ dependencies = [
146
146
 
147
147
  [[package]]
148
148
  name = "letify-driver"
149
- version = "2.6.3"
149
+ version = "2.6.4"
150
150
  dependencies = [
151
151
  "letify-wire",
152
152
  ]
@@ -164,7 +164,7 @@ dependencies = [
164
164
 
165
165
  [[package]]
166
166
  name = "letify-wire"
167
- version = "2.6.3"
167
+ version = "2.6.4"
168
168
 
169
169
  [[package]]
170
170
  name = "libc"
@@ -8,7 +8,7 @@ resolver = "3"
8
8
  members = ["letify-wire", "letify-driver", "letify-agent", "letify-quic"]
9
9
 
10
10
  [workspace.package]
11
- version = "2.6.3"
11
+ version = "2.6.4"
12
12
  edition = "2024"
13
13
  license = "MIT"
14
14
  repository = "https://github.com/DarkPyonix/letify"
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "letify-status",
3
- "version": "2.6.3",
3
+ "version": "2.6.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.6.3",
9
+ "version": "2.6.4",
10
10
  "license": "SEE LICENSE IN LICENSE",
11
11
  "devDependencies": {
12
12
  "@types/node": "^18.19.0",
@@ -2,7 +2,7 @@
2
2
  "name": "letify-status",
3
3
  "displayName": "letify Status",
4
4
  "description": "letify quota and GPU activity in the status bar, read from the letify CLI.",
5
- "version": "2.6.3",
5
+ "version": "2.6.4",
6
6
  "publisher": "darkpyonix",
7
7
  "license": "SEE LICENSE IN LICENSE",
8
8
  "repository": {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "letify"
3
- version = "2.6.3"
3
+ version = "2.6.4"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -2218,7 +2218,9 @@ def test_a_modal_sandbox_is_created_with_a_lifetime_and_an_idle_limit(
2218
2218
  provider.open_channel(runtime)
2219
2219
  try:
2220
2220
  [created] = fake_modal.requests("create")
2221
- assert created["timeout"] == 3600
2221
+ # Spec "Modal": the lifetime is the longest Modal accepts, not an hour letify
2222
+ # picked, because a deadline letify invented ends the user's work at its guess.
2223
+ assert created["timeout"] == 86400
2222
2224
  assert created["idle_timeout"] == 600
2223
2225
  finally:
2224
2226
  provider.stop(runtime)
@@ -2910,3 +2912,32 @@ def test_colab_punch_rendezvous_completes_during_tailcat_installation(
2910
2912
  release.set()
2911
2913
  installing.join(2)
2912
2914
  assert endpoints == [("fake", 22)]
2915
+
2916
+
2917
+ def test_a_modal_sandbox_is_not_given_an_hour_long_deadline(config_file) -> None:
2918
+ """Spec "Modal": a deadline letify invented would end a two hour run at its guess.
2919
+
2920
+ The default was 3600 s, exactly one hour, and it ended two Modal sessions of a real
2921
+ training run at one hour each with nothing saying why. The declaration's own `timeout`
2922
+ has no default for this reason, and the sandbox's lifetime is the same question.
2923
+ """
2924
+ from conftest import provider_of
2925
+
2926
+ from letify.providers import Modal
2927
+
2928
+ provider = provider_of(Modal, "lab")
2929
+ assert provider.sandbox_timeout_s == 86400, "letify picked a deadline again"
2930
+ # An idle sandbox is what has to be bounded, and that bound is unchanged.
2931
+ assert provider.sandbox_idle_timeout_s == 600
2932
+ # An account that wants a cap still sets one.
2933
+ assert provider_of(Modal, "lab", timeout=7200).sandbox_timeout_s == 7200
2934
+
2935
+
2936
+ def test_a_sandbox_that_reached_its_lifetime_says_so(config_file) -> None:
2937
+ """Spec "Modal": the channel otherwise lists causes, none of them this one."""
2938
+ from letify.providers.modal import lifetime_detail
2939
+
2940
+ assert lifetime_detail(age_s=3599.5, lifetime_s=3600) is not None
2941
+ assert "3600" in lifetime_detail(age_s=3599.5, lifetime_s=3600)
2942
+ assert "raise timeout" in lifetime_detail(age_s=3601.0, lifetime_s=3600)
2943
+ assert lifetime_detail(age_s=120.0, lifetime_s=3600) is None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes