letify 2.6.4__tar.gz → 2.6.6__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.4 → letify-2.6.6}/PKG-INFO +1 -1
  2. {letify-2.6.4 → letify-2.6.6}/docs/SPEC.md +21 -1
  3. {letify-2.6.4 → letify-2.6.6}/letify/__init__.py +1 -1
  4. {letify-2.6.4 → letify-2.6.6}/letify/runtime/session.py +33 -10
  5. {letify-2.6.4 → letify-2.6.6}/letify/store/pathdata.py +52 -2
  6. {letify-2.6.4 → letify-2.6.6}/letify-core/Cargo.lock +3 -3
  7. {letify-2.6.4 → letify-2.6.6}/letify-core/Cargo.toml +1 -1
  8. {letify-2.6.4 → letify-2.6.6}/letify-ext/package-lock.json +2 -2
  9. {letify-2.6.4 → letify-2.6.6}/letify-ext/package.json +1 -1
  10. {letify-2.6.4 → letify-2.6.6}/pyproject.toml +1 -1
  11. {letify-2.6.4 → letify-2.6.6}/tests/test_data_streaming.py +63 -0
  12. {letify-2.6.4 → letify-2.6.6}/tests/test_runtime.py +54 -0
  13. {letify-2.6.4 → letify-2.6.6}/.github/workflows/ci.yml +0 -0
  14. {letify-2.6.4 → letify-2.6.6}/.github/workflows/docs.yml +0 -0
  15. {letify-2.6.4 → letify-2.6.6}/.github/workflows/publish.yml +0 -0
  16. {letify-2.6.4 → letify-2.6.6}/.gitignore +0 -0
  17. {letify-2.6.4 → letify-2.6.6}/.researchtree +0 -0
  18. {letify-2.6.4 → letify-2.6.6}/CLAUDE.md +0 -0
  19. {letify-2.6.4 → letify-2.6.6}/LICENSE +0 -0
  20. {letify-2.6.4 → letify-2.6.6}/PROJECT.md +0 -0
  21. {letify-2.6.4 → letify-2.6.6}/README.md +0 -0
  22. {letify-2.6.4 → letify-2.6.6}/docs/COMPONENT.md +0 -0
  23. {letify-2.6.4 → letify-2.6.6}/docs/INTENT.md +0 -0
  24. {letify-2.6.4 → letify-2.6.6}/docs/NETWORK.md +0 -0
  25. {letify-2.6.4 → letify-2.6.6}/docs/guide/01-getting-started.md +0 -0
  26. {letify-2.6.4 → letify-2.6.6}/docs/guide/02-providers.md +0 -0
  27. {letify-2.6.4 → letify-2.6.6}/docs/guide/03-execution-modes.md +0 -0
  28. {letify-2.6.4 → letify-2.6.6}/docs/guide/04-environments-and-data.md +0 -0
  29. {letify-2.6.4 → letify-2.6.6}/docs/guide/05-concurrency.md +0 -0
  30. {letify-2.6.4 → letify-2.6.6}/docs/guide/06-cost.md +0 -0
  31. {letify-2.6.4 → letify-2.6.6}/docs/guide/07-troubleshooting.md +0 -0
  32. {letify-2.6.4 → letify-2.6.6}/docs/guide/08-releasing.md +0 -0
  33. {letify-2.6.4 → letify-2.6.6}/docs/guide/README.md +0 -0
  34. {letify-2.6.4 → letify-2.6.6}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  35. {letify-2.6.4 → letify-2.6.6}/docs/guide/ko/02-/355/224/204/353/241/234/353/260/224/354/235/264/353/215/224.md" +0 -0
  36. {letify-2.6.4 → letify-2.6.6}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  37. {letify-2.6.4 → letify-2.6.6}/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
  38. {letify-2.6.4 → letify-2.6.6}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  39. {letify-2.6.4 → letify-2.6.6}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  40. {letify-2.6.4 → letify-2.6.6}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  41. {letify-2.6.4 → letify-2.6.6}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  42. {letify-2.6.4 → letify-2.6.6}/docs/guide/ko/README.md +0 -0
  43. {letify-2.6.4 → letify-2.6.6}/docs/locales/README_ko.md +0 -0
  44. {letify-2.6.4 → letify-2.6.6}/docs/mkdocs.yml +0 -0
  45. {letify-2.6.4 → letify-2.6.6}/examples/README.md +0 -0
  46. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/.letify.example +0 -0
  47. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/00_smoke.py +0 -0
  48. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/01_sweep.py +0 -0
  49. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/02_resume.py +0 -0
  50. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/03_watch.py +0 -0
  51. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/README.md +0 -0
  52. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/README_ko.md +0 -0
  53. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/common.py +0 -0
  54. {letify-2.6.4 → letify-2.6.6}/examples/nvfp4-lora/recipe.py +0 -0
  55. {letify-2.6.4 → letify-2.6.6}/letify/cli.py +0 -0
  56. {letify-2.6.4 → letify-2.6.6}/letify/config/__init__.py +0 -0
  57. {letify-2.6.4 → letify-2.6.6}/letify/config/inventory.py +0 -0
  58. {letify-2.6.4 → letify-2.6.6}/letify/config/login.py +0 -0
  59. {letify-2.6.4 → letify-2.6.6}/letify/config/schema.py +0 -0
  60. {letify-2.6.4 → letify-2.6.6}/letify/config/secrets.py +0 -0
  61. {letify-2.6.4 → letify-2.6.6}/letify/config/writer.py +0 -0
  62. {letify-2.6.4 → letify-2.6.6}/letify/declare/__init__.py +0 -0
  63. {letify-2.6.4 → letify-2.6.6}/letify/declare/cache.py +0 -0
  64. {letify-2.6.4 → letify-2.6.6}/letify/declare/env.py +0 -0
  65. {letify-2.6.4 → letify-2.6.6}/letify/declare/function.py +0 -0
  66. {letify-2.6.4 → letify-2.6.6}/letify/declare/instance.py +0 -0
  67. {letify-2.6.4 → letify-2.6.6}/letify/declare/reduce.py +0 -0
  68. {letify-2.6.4 → letify-2.6.6}/letify/declare/replicas.py +0 -0
  69. {letify-2.6.4 → letify-2.6.6}/letify/errors.py +0 -0
  70. {letify-2.6.4 → letify-2.6.6}/letify/install.py +0 -0
  71. {letify-2.6.4 → letify-2.6.6}/letify/launcher.py +0 -0
  72. {letify-2.6.4 → letify-2.6.6}/letify/paths.py +0 -0
  73. {letify-2.6.4 → letify-2.6.6}/letify/protocol/__init__.py +0 -0
  74. {letify-2.6.4 → letify-2.6.6}/letify/protocol/codec.py +0 -0
  75. {letify-2.6.4 → letify-2.6.6}/letify/protocol/driver.py +0 -0
  76. {letify-2.6.4 → letify-2.6.6}/letify/protocol/handle.py +0 -0
  77. {letify-2.6.4 → letify-2.6.6}/letify/protocol/wire.py +0 -0
  78. {letify-2.6.4 → letify-2.6.6}/letify/protocol/worker.py +0 -0
  79. {letify-2.6.4 → letify-2.6.6}/letify/providers/__init__.py +0 -0
  80. {letify-2.6.4 → letify-2.6.6}/letify/providers/base.py +0 -0
  81. {letify-2.6.4 → letify-2.6.6}/letify/providers/colab.py +0 -0
  82. {letify-2.6.4 → letify-2.6.6}/letify/providers/colab_files.py +0 -0
  83. {letify-2.6.4 → letify-2.6.6}/letify/providers/colab_keepalive.py +0 -0
  84. {letify-2.6.4 → letify-2.6.6}/letify/providers/elice.py +0 -0
  85. {letify-2.6.4 → letify-2.6.6}/letify/providers/kaggle.py +0 -0
  86. {letify-2.6.4 → letify-2.6.6}/letify/providers/kaggle_adapter.py +0 -0
  87. {letify-2.6.4 → letify-2.6.6}/letify/providers/local.py +0 -0
  88. {letify-2.6.4 → letify-2.6.6}/letify/providers/modal.py +0 -0
  89. {letify-2.6.4 → letify-2.6.6}/letify/providers/modal_adapter.py +0 -0
  90. {letify-2.6.4 → letify-2.6.6}/letify/providers/naming.py +0 -0
  91. {letify-2.6.4 → letify-2.6.6}/letify/providers/shell.py +0 -0
  92. {letify-2.6.4 → letify-2.6.6}/letify/providers/tunnel.py +0 -0
  93. {letify-2.6.4 → letify-2.6.6}/letify/providers/usage.py +0 -0
  94. {letify-2.6.4 → letify-2.6.6}/letify/remoting/__init__.py +0 -0
  95. {letify-2.6.4 → letify-2.6.6}/letify/remoting/capability.py +0 -0
  96. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/__init__.py +0 -0
  97. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/client.py +0 -0
  98. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/cuda.py +0 -0
  99. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/executor.py +0 -0
  100. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/frames.py +0 -0
  101. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/guard.py +0 -0
  102. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/tensor.py +0 -0
  103. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/trace.py +0 -0
  104. {letify-2.6.4 → letify-2.6.6}/letify/remoting/device/value.py +0 -0
  105. {letify-2.6.4 → letify-2.6.6}/letify/remoting/loader.py +0 -0
  106. {letify-2.6.4 → letify-2.6.6}/letify/remoting/probe.py +0 -0
  107. {letify-2.6.4 → letify-2.6.6}/letify/render.py +0 -0
  108. {letify-2.6.4 → letify-2.6.6}/letify/runtime/__init__.py +0 -0
  109. {letify-2.6.4 → letify-2.6.6}/letify/runtime/bootstrap.py +0 -0
  110. {letify-2.6.4 → letify-2.6.6}/letify/runtime/broker.py +0 -0
  111. {letify-2.6.4 → letify-2.6.6}/letify/runtime/channel.py +0 -0
  112. {letify-2.6.4 → letify-2.6.6}/letify/runtime/lease.py +0 -0
  113. {letify-2.6.4 → letify-2.6.6}/letify/runtime/pool.py +0 -0
  114. {letify-2.6.4 → letify-2.6.6}/letify/runtime/telemetry.py +0 -0
  115. {letify-2.6.4 → letify-2.6.6}/letify/store/__init__.py +0 -0
  116. {letify-2.6.4 → letify-2.6.6}/letify/store/backends/__init__.py +0 -0
  117. {letify-2.6.4 → letify-2.6.6}/letify/store/backends/filesystem.py +0 -0
  118. {letify-2.6.4 → letify-2.6.6}/letify/store/backends/google_auth.py +0 -0
  119. {letify-2.6.4 → letify-2.6.6}/letify/store/backends/layout.py +0 -0
  120. {letify-2.6.4 → letify-2.6.6}/letify/store/backends/objects.py +0 -0
  121. {letify-2.6.4 → letify-2.6.6}/letify/store/cas.py +0 -0
  122. {letify-2.6.4 → letify-2.6.6}/letify/store/sendorder.py +0 -0
  123. {letify-2.6.4 → letify-2.6.6}/letify/store/volume.py +0 -0
  124. {letify-2.6.4 → letify-2.6.6}/letify/stubs.py +0 -0
  125. {letify-2.6.4 → letify-2.6.6}/letify/tools.py +0 -0
  126. {letify-2.6.4 → letify-2.6.6}/letify/transport/__init__.py +0 -0
  127. {letify-2.6.4 → letify-2.6.6}/letify/transport/agent.py +0 -0
  128. {letify-2.6.4 → letify-2.6.6}/letify/transport/announce.py +0 -0
  129. {letify-2.6.4 → letify-2.6.6}/letify/transport/link.py +0 -0
  130. {letify-2.6.4 → letify-2.6.6}/letify/transport/nat.py +0 -0
  131. {letify-2.6.4 → letify-2.6.6}/letify/transport/pipeline.py +0 -0
  132. {letify-2.6.4 → letify-2.6.6}/letify/transport/portmap.py +0 -0
  133. {letify-2.6.4 → letify-2.6.6}/letify/transport/probe.py +0 -0
  134. {letify-2.6.4 → letify-2.6.6}/letify/transport/quic.py +0 -0
  135. {letify-2.6.4 → letify-2.6.6}/letify/transport/rendezvous.py +0 -0
  136. {letify-2.6.4 → letify-2.6.6}/letify/transport/setup.py +0 -0
  137. {letify-2.6.4 → letify-2.6.6}/letify/transport/sshopts.py +0 -0
  138. {letify-2.6.4 → letify-2.6.6}/letify/transport/strategies.py +0 -0
  139. {letify-2.6.4 → letify-2.6.6}/letify-core/.gitignore +0 -0
  140. {letify-2.6.4 → letify-2.6.6}/letify-core/build.py +0 -0
  141. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-agent/Cargo.toml +0 -0
  142. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-agent/src/driver.rs +0 -0
  143. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-agent/src/main.rs +0 -0
  144. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-driver/Cargo.toml +0 -0
  145. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-driver/src/client.rs +0 -0
  146. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-driver/src/exports.rs +0 -0
  147. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-driver/src/image.rs +0 -0
  148. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-driver/src/lib.rs +0 -0
  149. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-driver/src/status.rs +0 -0
  150. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-driver/src/table.rs +0 -0
  151. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-quic/Cargo.toml +0 -0
  152. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-quic/src/main.rs +0 -0
  153. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-wire/Cargo.toml +0 -0
  154. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-wire/src/lib.rs +0 -0
  155. {letify-2.6.4 → letify-2.6.6}/letify-core/letify-wire/tests/throughput.rs +0 -0
  156. {letify-2.6.4 → letify-2.6.6}/letify-core/rust-toolchain.toml +0 -0
  157. {letify-2.6.4 → letify-2.6.6}/letify-ext/.vscodeignore +0 -0
  158. {letify-2.6.4 → letify-2.6.6}/letify-ext/LICENSE +0 -0
  159. {letify-2.6.4 → letify-2.6.6}/letify-ext/README.md +0 -0
  160. {letify-2.6.4 → letify-2.6.6}/letify-ext/media/icon.svg +0 -0
  161. {letify-2.6.4 → letify-2.6.6}/letify-ext/src/cli.ts +0 -0
  162. {letify-2.6.4 → letify-2.6.6}/letify-ext/src/extension.ts +0 -0
  163. {letify-2.6.4 → letify-2.6.6}/letify-ext/src/history.ts +0 -0
  164. {letify-2.6.4 → letify-2.6.6}/letify-ext/src/model.ts +0 -0
  165. {letify-2.6.4 → letify-2.6.6}/letify-ext/src/poll.ts +0 -0
  166. {letify-2.6.4 → letify-2.6.6}/letify-ext/src/render.ts +0 -0
  167. {letify-2.6.4 → letify-2.6.6}/letify-ext/test/cli.test.ts +0 -0
  168. {letify-2.6.4 → letify-2.6.6}/letify-ext/test/manifest.test.ts +0 -0
  169. {letify-2.6.4 → letify-2.6.6}/letify-ext/test/model.test.ts +0 -0
  170. {letify-2.6.4 → letify-2.6.6}/letify-ext/test/regressions.test.ts +0 -0
  171. {letify-2.6.4 → letify-2.6.6}/letify-ext/tsconfig.json +0 -0
  172. {letify-2.6.4 → letify-2.6.6}/letify_providers.py +0 -0
  173. {letify-2.6.4 → letify-2.6.6}/scripts/sync-main.sh +0 -0
  174. {letify-2.6.4 → letify-2.6.6}/scripts/version.py +0 -0
  175. {letify-2.6.4 → letify-2.6.6}/tests/conftest.py +0 -0
  176. {letify-2.6.4 → letify-2.6.6}/tests/fake_kaggle_adapter.py +0 -0
  177. {letify-2.6.4 → letify-2.6.6}/tests/fake_modal_adapter.py +0 -0
  178. {letify-2.6.4 → letify-2.6.6}/tests/test_cli.py +0 -0
  179. {letify-2.6.4 → letify-2.6.6}/tests/test_cli_output.py +0 -0
  180. {letify-2.6.4 → letify-2.6.6}/tests/test_colab_keepalive.py +0 -0
  181. {letify-2.6.4 → letify-2.6.6}/tests/test_config.py +0 -0
  182. {letify-2.6.4 → letify-2.6.6}/tests/test_core.py +0 -0
  183. {letify-2.6.4 → letify-2.6.6}/tests/test_data.py +0 -0
  184. {letify-2.6.4 → letify-2.6.6}/tests/test_declare.py +0 -0
  185. {letify-2.6.4 → letify-2.6.6}/tests/test_device.py +0 -0
  186. {letify-2.6.4 → letify-2.6.6}/tests/test_distributed.py +0 -0
  187. {letify-2.6.4 → letify-2.6.6}/tests/test_editor_sessions.py +0 -0
  188. {letify-2.6.4 → letify-2.6.6}/tests/test_elice.py +0 -0
  189. {letify-2.6.4 → letify-2.6.6}/tests/test_frames.py +0 -0
  190. {letify-2.6.4 → letify-2.6.6}/tests/test_install.py +0 -0
  191. {letify-2.6.4 → letify-2.6.6}/tests/test_kaggle.py +0 -0
  192. {letify-2.6.4 → letify-2.6.6}/tests/test_kaggle_cookie.py +0 -0
  193. {letify-2.6.4 → letify-2.6.6}/tests/test_kaggle_link.py +0 -0
  194. {letify-2.6.4 → letify-2.6.6}/tests/test_link_log.py +0 -0
  195. {letify-2.6.4 → letify-2.6.6}/tests/test_notebook.py +0 -0
  196. {letify-2.6.4 → letify-2.6.6}/tests/test_parallel_placement.py +0 -0
  197. {letify-2.6.4 → letify-2.6.6}/tests/test_paths.py +0 -0
  198. {letify-2.6.4 → letify-2.6.6}/tests/test_portmap.py +0 -0
  199. {letify-2.6.4 → letify-2.6.6}/tests/test_protocol.py +0 -0
  200. {letify-2.6.4 → letify-2.6.6}/tests/test_providers.py +0 -0
  201. {letify-2.6.4 → letify-2.6.6}/tests/test_quic.py +0 -0
  202. {letify-2.6.4 → letify-2.6.6}/tests/test_remoting.py +0 -0
  203. {letify-2.6.4 → letify-2.6.6}/tests/test_render.py +0 -0
  204. {letify-2.6.4 → letify-2.6.6}/tests/test_ship.py +0 -0
  205. {letify-2.6.4 → letify-2.6.6}/tests/test_store.py +0 -0
  206. {letify-2.6.4 → letify-2.6.6}/tests/test_strategies.py +0 -0
  207. {letify-2.6.4 → letify-2.6.6}/tests/test_stubs.py +0 -0
  208. {letify-2.6.4 → letify-2.6.6}/tests/test_transport.py +0 -0
  209. {letify-2.6.4 → letify-2.6.6}/tests/test_tunnel_setup.py +0 -0
  210. {letify-2.6.4 → letify-2.6.6}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.6.4
3
+ Version: 2.6.6
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
@@ -1063,6 +1063,16 @@ less time than the build did. The archive is smaller than the directory, so the
1063
1063
  overstates the transfer and errs toward building, which is the cheaper of the two to get wrong. The
1064
1064
  ref records the build seconds beside the archive.
1065
1065
 
1066
+ With no measured rate for the link, letify packs only when that directory is at most 1 GiB, and says
1067
+ why when it declines. Packing moves the archive as one payload that the worker and the client each
1068
+ hold whole, so a tree of several gigabytes is held twice in memory with nothing reporting progress:
1069
+ measured on a Modal A10, a 7.2 GB environment sat in that step for over 30 minutes while the session
1070
+ billed. Packing unconditionally was right while only an ephemeral provider packed, where the
1071
+ alternative was a certain full rebuild; a provider that keeps its workspace root and builds its venv
1072
+ elsewhere reaches the same branch, and there the alternative is a sync that may take two minutes.
1073
+ Making the pack stream rather than hold the archive whole is the fix that removes this limit, and
1074
+ until then the limit is what keeps the step from costing more than it saves.
1075
+
1066
1076
  A later session restores only when the archive's size over the link measured for that session would
1067
1077
  take less than the recorded build. Otherwise it syncs, and says why:
1068
1078
  `letify: <runtime>: syncing rather than restoring, because 3.0 GB over 13.6 MiB/s is 210 s against a
@@ -1076,7 +1086,9 @@ the kind this rule exists to catch, and the session that syncs instead both reco
1076
1086
  decides afresh whether the archive is worth keeping. The cost of that is one sync; the cost of
1077
1087
  trusting it is the transfer this rule was measured to avoid.
1078
1088
 
1079
- Skipping the pack is said too, because it is a decision letify made rather than a thing that did not
1089
+ Packing is said, because it is a step that can take minutes and had nothing reporting it:
1090
+ `letify: <runtime>: caching the environment, 2.1 GB`. Skipping it is said too, because it is a
1091
+ decision letify made rather than a thing that did not
1080
1092
  happen: `letify: <runtime>: not caching the environment, because 9.4 GB over 13.6 MiB/s is 659 s
1081
1093
  against a 34 s build`.
1082
1094
 
@@ -1353,6 +1365,14 @@ A call that detected data prints one line on standard error:
1353
1365
 
1354
1366
  The line is printed when the call has at least one input file, after the call's outcome is known, because what was sent during the call is not known before then. Sizes are in MiB with one decimal. `<the bucket>` is named when the bytes come from the bucket. `before the call` counts the first wave of The send order and the first wave, `during it` counts what the background sender delivered while the call ran, and the rate covers both. `first access waited` is the wall clock time in which at least one reader was blocked on a blob that had not arrived, and it is `0.0` when the send order was right.
1355
1367
 
1368
+ The bytes of a blob still on the way are reported too, because the rate counts only blobs that
1369
+ completed and a single large blob in flight therefore reads as `0.0 MiB/s` however fast it is moving.
1370
+ A line whose dropped clause names files ends with how far they got,
1371
+ `, 3 files 14985.8 MiB dropped when the call ended, 4512.0 MiB of them had arrived`, and the
1372
+ background sender draws the same `uploading` progress line the first wave draws, which it did not
1373
+ before: a blob sent beside the call reported nothing at all, so a transfer in progress and a transfer
1374
+ that had never started looked the same. Both cost a reader an afternoon.
1375
+
1356
1376
  `already on` counts the blobs the runtime answered `data_have` with, not what is left after subtracting what was sent. The two differ whenever a blob was neither already there nor fully sent, and subtracting reports such a blob as having been there all along, which is the opposite of what happened. A blob the background sender was still carrying when the call returned is one of those, so the line ends with `, <files> files <size> dropped when the call ended` whenever there is one, and omits that clause when there is none. A dropped blob is not a failure: the call is over and its bytes are no longer needed, which is why the sender is cancelled. It is reported because a line that accounts for every detected file except the dropped ones cannot be read.
1357
1377
 
1358
1378
  Cancelling the sender removes the partial file of a blob it was writing, in the runtime's blob cache as well as a parallel put's staged ranges. A full sized partial left behind counts against the cache budget and is never completed, because the digest it would be sealed under is only written when every range has arrived.
@@ -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.4"
69
+ __version__ = "2.6.6"
70
70
 
71
71
 
72
72
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -47,6 +47,11 @@ if TYPE_CHECKING:
47
47
  #: The total size of argument blob files kept under a persistent workspace root, 32 GiB.
48
48
  BLOB_DISK_LIMIT = 32 * 1024**3
49
49
 
50
+ #: The largest project directory letify packs into an environment archive when it has no
51
+ #: measured rate to judge the transfer by. Spec "Materializing into a runtime": the pack
52
+ #: moves the archive as one payload the worker and the client each hold whole.
53
+ PACK_WITHOUT_A_RATE = 1024**3
54
+
50
55
 
51
56
  def _against_the_build(size: int, rate: float, build_s: float) -> str:
52
57
  """The three numbers both cache decisions weigh, as they are reported.
@@ -185,6 +190,11 @@ class Runtime:
185
190
  self.provider.env_root or self.workspace or self.provider.workspace_root, self.env
186
191
  )
187
192
  if self._worth_packing(root):
193
+ size = self._tree_size(root)
194
+ self._say(
195
+ "caching the environment"
196
+ + (f", {size / 1_000_000_000:.1f} GB" if size else "")
197
+ )
188
198
  self._environment_volumes()[0].cache_env_from(
189
199
  self, self.env, root, platform=self.platform, build_s=self.env_build_s
190
200
  )
@@ -687,25 +697,38 @@ class Runtime:
687
697
  return None
688
698
  return float(rate)
689
699
 
700
+ def _tree_size(self, root: str) -> int | None:
701
+ """The byte size of the project directory on the runtime, or None when unknown."""
702
+ from . import bootstrap
703
+
704
+ try:
705
+ size = self.eval(bootstrap.tree_size_source(root), timeout=120)
706
+ except Exception:
707
+ return None
708
+ return size if isinstance(size, int) and size > 0 else None
709
+
690
710
  def _worth_packing(self, root: str) -> bool:
691
711
  """Whether moving the project directory over this session's link beats the build.
692
712
 
693
713
  Spec "Materializing into a runtime": the directory is larger than the archive it
694
- would become, so this overstates the transfer and errs toward building. A session
695
- with no measured rate packs unconditionally, as it always did.
714
+ would become, so this overstates the transfer and errs toward building. With no
715
+ measured rate it packs only what letify is willing to hold in memory, because the
716
+ pack moves the archive as one payload the worker and the client each hold whole.
696
717
  """
697
718
  if self.env_build_s is None:
698
719
  return True
699
720
  rate = self._measured_rate()
721
+ size = self._tree_size(root)
700
722
  if rate is None:
701
- return True
702
- from . import bootstrap
703
-
704
- try:
705
- size = self.eval(bootstrap.tree_size_source(root), timeout=120)
706
- except Exception:
707
- return True
708
- if not isinstance(size, int) or size <= 0:
723
+ if size is None or size <= PACK_WITHOUT_A_RATE:
724
+ return True
725
+ self._say(
726
+ f"not caching the environment, because {size / 1_000_000_000:.1f} GB is over "
727
+ f"the {PACK_WITHOUT_A_RATE / 1024**3:.1f} GiB letify will hold in memory and "
728
+ f"this link was never measured"
729
+ )
730
+ return False
731
+ if size is None:
709
732
  return True
710
733
  if (size / rate) < self.env_build_s:
711
734
  return True
@@ -485,6 +485,32 @@ def _mib(size: int) -> str:
485
485
  return f"{size / (1 << 20):.1f} MiB"
486
486
 
487
487
 
488
+ class _Carried:
489
+ """One blob's progress, recorded on the stream and forwarded to the real meter.
490
+
491
+ Spec "Data log line": the bytes of a blob still on the way are reported, and only the
492
+ blob's own sender knows how far it got.
493
+ """
494
+
495
+ def __init__(self, stream: Any, digest: str, meter: Any) -> None:
496
+ self._stream = stream
497
+ self._digest = digest
498
+ self._meter = meter
499
+
500
+ def update(self, done: int) -> None:
501
+ self._stream.carried[self._digest] = done
502
+ if self._meter is not None:
503
+ self._meter.update(done)
504
+
505
+ def advance(self, size: int) -> None:
506
+ if self._meter is not None:
507
+ self._meter.advance(size)
508
+
509
+ def finish(self) -> None:
510
+ if self._meter is not None:
511
+ self._meter.finish()
512
+
513
+
488
514
  class _Uploaded:
489
515
  """Bytes sent so far, drawn as a progress line when the upload is large.
490
516
 
@@ -626,6 +652,10 @@ class Stream:
626
652
  #: Digests whose send finished. ``_sent`` holds the ones it began, so a blob the
627
653
  #: sender was carrying when the call ended is in one and not the other.
628
654
  self.done: set[str] = set()
655
+ #: Bytes of each blob handed to the runtime, including one still on the way.
656
+ self.carried: dict[str, int] = {}
657
+ #: The meter for blobs sent beside the call, spec "Data log line".
658
+ self._meter: _Uploaded | None = None
629
659
  self.before_files = self.before_bytes = 0
630
660
  self.during_files = self.during_bytes = 0
631
661
  self.before_seconds = self.during_seconds = 0.0
@@ -750,9 +780,22 @@ class Stream:
750
780
  digest = self._take()
751
781
  if digest is None:
752
782
  return False
753
- self._send(digest, None)
783
+ self._send(digest, self._beside())
754
784
  return True
755
785
 
786
+ def _beside(self) -> _Uploaded:
787
+ """The meter for blobs sent beside the call, made at the first one.
788
+
789
+ Spec "Data log line": without one, a blob sent beside the call reported nothing,
790
+ so a transfer in progress and one that never started looked the same.
791
+ """
792
+ with self._lock:
793
+ if self._meter is None:
794
+ left = [d for d in self.plan.missing if d not in self.done]
795
+ total = sum(self.plan.weight(digest)[1] for digest in left)
796
+ self._meter = _Uploaded(len(left), total)
797
+ return self._meter
798
+
756
799
  def _take(self) -> str | None:
757
800
  with self._lock:
758
801
  while self._queue:
@@ -916,6 +959,7 @@ class Stream:
916
959
  size = self.plan.sizes.get(digest, 0)
917
960
  local = self.plan.files[digest]
918
961
  bucket = self.plan.bucket
962
+ meter = _Carried(self, digest, meter)
919
963
  uploaded = True
920
964
  if bucket is None:
921
965
  # The cost of opening the connections is weighed only while the pool is cold:
@@ -1014,9 +1058,13 @@ class Stream:
1014
1058
  missing_weight = [plan.weight(digest) for digest in plan.missing]
1015
1059
  already_files = plan.detected_files - sum(files for files, _b in missing_weight)
1016
1060
  already_bytes = plan.detected_bytes - sum(total for _f, total in missing_weight)
1017
- dropped_weight = [plan.weight(digest) for digest in self.dropped]
1061
+ dropped = self.dropped
1062
+ dropped_weight = [plan.weight(digest) for digest in dropped]
1018
1063
  dropped_files = sum(files for files, _b in dropped_weight)
1019
1064
  dropped_bytes = sum(total for _f, total in dropped_weight)
1065
+ # Spec "Data log line": the rate counts only blobs that completed, so a large one
1066
+ # in flight reads as 0.0 MiB/s however fast it is moving.
1067
+ arrived = sum(self.carried.get(digest, 0) for digest in dropped)
1020
1068
  line = (
1021
1069
  f"data {plan.detected_files} files {_mib(plan.detected_bytes)} detected, "
1022
1070
  f"{already_files} files {_mib(already_bytes)} already on {plan.where}, "
@@ -1029,6 +1077,8 @@ class Stream:
1029
1077
  line += (
1030
1078
  f", {dropped_files} files {_mib(dropped_bytes)} dropped when the call ended"
1031
1079
  )
1080
+ if arrived:
1081
+ line += f", {_mib(arrived)} of them had arrived"
1032
1082
  install.log(line)
1033
1083
  if stats.get("files"):
1034
1084
  install.log(
@@ -138,7 +138,7 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify-agent"
141
- version = "2.6.4"
141
+ version = "2.6.6"
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.4"
149
+ version = "2.6.6"
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.4"
167
+ version = "2.6.6"
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.4"
11
+ version = "2.6.6"
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.4",
3
+ "version": "2.6.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.6.4",
9
+ "version": "2.6.6",
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.4",
5
+ "version": "2.6.6",
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.4"
3
+ version = "2.6.6"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1757,3 +1757,66 @@ def test_a_blob_that_never_starts_still_times_out_and_says_how_far_it_got(
1757
1757
  message = str(failure.value)
1758
1758
  assert "did not arrive" in message, message
1759
1759
  assert "arrived" in message, message
1760
+
1761
+
1762
+ def test_a_blob_sent_beside_the_call_reports_its_bytes(
1763
+ let, cpu, project, capsys, monkeypatch
1764
+ ) -> None:
1765
+ """Spec "Data log line": a transfer in progress read the same as one never started.
1766
+
1767
+ KICT's 11.3 GB table was moving the whole time and the line said 0.0 MiB/s, because
1768
+ the rate counts only blobs that completed and the background sender was given no
1769
+ meter at all.
1770
+ """
1771
+ from letify.store import pathdata
1772
+
1773
+ big = project / "beside.bin"
1774
+ big.write_bytes(b"q" * (3 << 20))
1775
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_MIB", 0.0)
1776
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_FILES", 0)
1777
+ monkeypatch.setattr(pathdata, "WAIT_TIMEOUT_S", 4.0)
1778
+ def stalled(runtime, pipeline, blobs, digest, local, size, meter):
1779
+ # A third of the blob is reported as handed over and then the send stops, which
1780
+ # is the shape of a blob the sender was carrying when the call ended.
1781
+ meter.update(1 << 20)
1782
+ raise RuntimeFailure("the transfer stream failed")
1783
+
1784
+ monkeypatch.setattr(pathdata, "_put", stalled)
1785
+
1786
+ @let.function(device=cpu, host=letify.remote, retries=0)
1787
+ def peek(path) -> int:
1788
+ return len(path.read_bytes())
1789
+
1790
+ with pytest.raises(RuntimeFailure):
1791
+ peek(path=big)
1792
+ lines = [
1793
+ text
1794
+ for text in capsys.readouterr().err.splitlines()
1795
+ if text.startswith("letify: data ") and "detected," in text
1796
+ ]
1797
+ assert lines
1798
+ assert "dropped when the call ended" in lines[0], lines[0]
1799
+ assert "had arrived" in lines[0], lines[0]
1800
+
1801
+
1802
+ def test_a_large_blob_beside_the_call_draws_the_progress_line(
1803
+ let, cpu, project, capsys, monkeypatch
1804
+ ) -> None:
1805
+ """Spec "Data log line": the background sender draws the same line the wave draws.
1806
+
1807
+ KICT reported no 'uploading' line at all while 11.3 GB moved beside a call, because
1808
+ the background sender was given no meter.
1809
+ """
1810
+ from letify.store import pathdata
1811
+
1812
+ big = project / "wide.bin"
1813
+ big.write_bytes(b"w" * (70 << 20))
1814
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_MIB", 0.0)
1815
+ monkeypatch.setattr(pathdata, "FIRST_WAVE_FILES", 0)
1816
+
1817
+ @let.function(device=cpu, host=letify.remote)
1818
+ def read_it(path) -> int:
1819
+ return len(path.read_bytes())
1820
+
1821
+ assert read_it(path=big) == 70 << 20
1822
+ assert "uploading" in capsys.readouterr().err
@@ -2593,3 +2593,57 @@ def test_a_persistent_provider_that_builds_its_venv_elsewhere_still_archives(
2593
2593
  finally:
2594
2594
  runtime.shutdown()
2595
2595
  assert volume.cached_env(env, platform), "nothing was archived, so the next session resyncs"
2596
+
2597
+
2598
+ def test_a_large_environment_is_not_packed_when_the_link_was_never_measured(
2599
+ uv_project: Path, tmp_path: Path, monkeypatch, capsys
2600
+ ) -> None:
2601
+ """Spec "Materializing into a runtime": the pack holds the archive whole, twice.
2602
+
2603
+ 2.6.3 made a provider that keeps its workspace root archive like an ephemeral one, and
2604
+ that branch packed unconditionally with no measured rate. On a Modal A10 a 7.2 GB
2605
+ environment sat in the pack for over 30 minutes, silently, while the session billed.
2606
+ """
2607
+ from letify.runtime import session as sessionmod
2608
+
2609
+ provider = provider_of(PreparingLocal, "lab", persistent=False)
2610
+ volume = provider.volume(
2611
+ "cache", backend="filesystem", root=str(tmp_path / "store"), mount=str(tmp_path / "mount")
2612
+ )
2613
+ env = Env()
2614
+ instance = Instance(provider, gpu=None)._placed("remote")
2615
+ # No measured rate, as a Modal sandbox has none, and a tree over the limit.
2616
+ monkeypatch.setattr(sessionmod.Runtime, "_measured_rate", lambda self: None)
2617
+ monkeypatch.setattr(
2618
+ sessionmod.Runtime, "_tree_size", lambda self, root: 8 * 1024**3
2619
+ )
2620
+ runtime = provider.start(instance, env, name="lab-1", volumes=(volume,))
2621
+ try:
2622
+ platform = runtime.platform
2623
+ finally:
2624
+ runtime.shutdown()
2625
+ assert volume.cached_env(env, platform) is None
2626
+ err = capsys.readouterr().err
2627
+ assert "over the 1.0 GiB letify will hold in memory" in err, err
2628
+
2629
+
2630
+ def test_a_small_environment_is_still_packed_and_says_so(
2631
+ uv_project: Path, tmp_path: Path, monkeypatch, capsys
2632
+ ) -> None:
2633
+ """Spec "Materializing into a runtime": the pack can take minutes, so it is said."""
2634
+ from letify.runtime import session as sessionmod
2635
+
2636
+ provider = provider_of(PreparingLocal, "lab", persistent=False)
2637
+ volume = provider.volume(
2638
+ "cache", backend="filesystem", root=str(tmp_path / "store"), mount=str(tmp_path / "mount")
2639
+ )
2640
+ env = Env()
2641
+ instance = Instance(provider, gpu=None)._placed("remote")
2642
+ monkeypatch.setattr(sessionmod.Runtime, "_measured_rate", lambda self: None)
2643
+ runtime = provider.start(instance, env, name="lab-1", volumes=(volume,))
2644
+ try:
2645
+ platform = runtime.platform
2646
+ finally:
2647
+ runtime.shutdown()
2648
+ assert volume.cached_env(env, platform)
2649
+ assert "caching the environment" in capsys.readouterr().err
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