letify 2.6.5__tar.gz → 2.6.7__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.5 → letify-2.6.7}/PKG-INFO +1 -1
  2. {letify-2.6.5 → letify-2.6.7}/docs/SPEC.md +22 -1
  3. {letify-2.6.5 → letify-2.6.7}/letify/__init__.py +1 -1
  4. {letify-2.6.5 → letify-2.6.7}/letify/providers/modal.py +33 -3
  5. {letify-2.6.5 → letify-2.6.7}/letify/store/pathdata.py +52 -2
  6. {letify-2.6.5 → letify-2.6.7}/letify-core/Cargo.lock +3 -3
  7. {letify-2.6.5 → letify-2.6.7}/letify-core/Cargo.toml +1 -1
  8. {letify-2.6.5 → letify-2.6.7}/letify-ext/package-lock.json +2 -2
  9. {letify-2.6.5 → letify-2.6.7}/letify-ext/package.json +1 -1
  10. {letify-2.6.5 → letify-2.6.7}/pyproject.toml +1 -1
  11. {letify-2.6.5 → letify-2.6.7}/tests/test_data_streaming.py +63 -0
  12. {letify-2.6.5 → letify-2.6.7}/tests/test_providers.py +52 -0
  13. {letify-2.6.5 → letify-2.6.7}/.github/workflows/ci.yml +0 -0
  14. {letify-2.6.5 → letify-2.6.7}/.github/workflows/docs.yml +0 -0
  15. {letify-2.6.5 → letify-2.6.7}/.github/workflows/publish.yml +0 -0
  16. {letify-2.6.5 → letify-2.6.7}/.gitignore +0 -0
  17. {letify-2.6.5 → letify-2.6.7}/.researchtree +0 -0
  18. {letify-2.6.5 → letify-2.6.7}/CLAUDE.md +0 -0
  19. {letify-2.6.5 → letify-2.6.7}/LICENSE +0 -0
  20. {letify-2.6.5 → letify-2.6.7}/PROJECT.md +0 -0
  21. {letify-2.6.5 → letify-2.6.7}/README.md +0 -0
  22. {letify-2.6.5 → letify-2.6.7}/docs/COMPONENT.md +0 -0
  23. {letify-2.6.5 → letify-2.6.7}/docs/INTENT.md +0 -0
  24. {letify-2.6.5 → letify-2.6.7}/docs/NETWORK.md +0 -0
  25. {letify-2.6.5 → letify-2.6.7}/docs/guide/01-getting-started.md +0 -0
  26. {letify-2.6.5 → letify-2.6.7}/docs/guide/02-providers.md +0 -0
  27. {letify-2.6.5 → letify-2.6.7}/docs/guide/03-execution-modes.md +0 -0
  28. {letify-2.6.5 → letify-2.6.7}/docs/guide/04-environments-and-data.md +0 -0
  29. {letify-2.6.5 → letify-2.6.7}/docs/guide/05-concurrency.md +0 -0
  30. {letify-2.6.5 → letify-2.6.7}/docs/guide/06-cost.md +0 -0
  31. {letify-2.6.5 → letify-2.6.7}/docs/guide/07-troubleshooting.md +0 -0
  32. {letify-2.6.5 → letify-2.6.7}/docs/guide/08-releasing.md +0 -0
  33. {letify-2.6.5 → letify-2.6.7}/docs/guide/README.md +0 -0
  34. {letify-2.6.5 → letify-2.6.7}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  35. {letify-2.6.5 → letify-2.6.7}/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.5 → letify-2.6.7}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  37. {letify-2.6.5 → letify-2.6.7}/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.5 → letify-2.6.7}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  39. {letify-2.6.5 → letify-2.6.7}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  40. {letify-2.6.5 → letify-2.6.7}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  41. {letify-2.6.5 → letify-2.6.7}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  42. {letify-2.6.5 → letify-2.6.7}/docs/guide/ko/README.md +0 -0
  43. {letify-2.6.5 → letify-2.6.7}/docs/locales/README_ko.md +0 -0
  44. {letify-2.6.5 → letify-2.6.7}/docs/mkdocs.yml +0 -0
  45. {letify-2.6.5 → letify-2.6.7}/examples/README.md +0 -0
  46. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/.letify.example +0 -0
  47. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/00_smoke.py +0 -0
  48. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/01_sweep.py +0 -0
  49. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/02_resume.py +0 -0
  50. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/03_watch.py +0 -0
  51. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/README.md +0 -0
  52. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/README_ko.md +0 -0
  53. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/common.py +0 -0
  54. {letify-2.6.5 → letify-2.6.7}/examples/nvfp4-lora/recipe.py +0 -0
  55. {letify-2.6.5 → letify-2.6.7}/letify/cli.py +0 -0
  56. {letify-2.6.5 → letify-2.6.7}/letify/config/__init__.py +0 -0
  57. {letify-2.6.5 → letify-2.6.7}/letify/config/inventory.py +0 -0
  58. {letify-2.6.5 → letify-2.6.7}/letify/config/login.py +0 -0
  59. {letify-2.6.5 → letify-2.6.7}/letify/config/schema.py +0 -0
  60. {letify-2.6.5 → letify-2.6.7}/letify/config/secrets.py +0 -0
  61. {letify-2.6.5 → letify-2.6.7}/letify/config/writer.py +0 -0
  62. {letify-2.6.5 → letify-2.6.7}/letify/declare/__init__.py +0 -0
  63. {letify-2.6.5 → letify-2.6.7}/letify/declare/cache.py +0 -0
  64. {letify-2.6.5 → letify-2.6.7}/letify/declare/env.py +0 -0
  65. {letify-2.6.5 → letify-2.6.7}/letify/declare/function.py +0 -0
  66. {letify-2.6.5 → letify-2.6.7}/letify/declare/instance.py +0 -0
  67. {letify-2.6.5 → letify-2.6.7}/letify/declare/reduce.py +0 -0
  68. {letify-2.6.5 → letify-2.6.7}/letify/declare/replicas.py +0 -0
  69. {letify-2.6.5 → letify-2.6.7}/letify/errors.py +0 -0
  70. {letify-2.6.5 → letify-2.6.7}/letify/install.py +0 -0
  71. {letify-2.6.5 → letify-2.6.7}/letify/launcher.py +0 -0
  72. {letify-2.6.5 → letify-2.6.7}/letify/paths.py +0 -0
  73. {letify-2.6.5 → letify-2.6.7}/letify/protocol/__init__.py +0 -0
  74. {letify-2.6.5 → letify-2.6.7}/letify/protocol/codec.py +0 -0
  75. {letify-2.6.5 → letify-2.6.7}/letify/protocol/driver.py +0 -0
  76. {letify-2.6.5 → letify-2.6.7}/letify/protocol/handle.py +0 -0
  77. {letify-2.6.5 → letify-2.6.7}/letify/protocol/wire.py +0 -0
  78. {letify-2.6.5 → letify-2.6.7}/letify/protocol/worker.py +0 -0
  79. {letify-2.6.5 → letify-2.6.7}/letify/providers/__init__.py +0 -0
  80. {letify-2.6.5 → letify-2.6.7}/letify/providers/base.py +0 -0
  81. {letify-2.6.5 → letify-2.6.7}/letify/providers/colab.py +0 -0
  82. {letify-2.6.5 → letify-2.6.7}/letify/providers/colab_files.py +0 -0
  83. {letify-2.6.5 → letify-2.6.7}/letify/providers/colab_keepalive.py +0 -0
  84. {letify-2.6.5 → letify-2.6.7}/letify/providers/elice.py +0 -0
  85. {letify-2.6.5 → letify-2.6.7}/letify/providers/kaggle.py +0 -0
  86. {letify-2.6.5 → letify-2.6.7}/letify/providers/kaggle_adapter.py +0 -0
  87. {letify-2.6.5 → letify-2.6.7}/letify/providers/local.py +0 -0
  88. {letify-2.6.5 → letify-2.6.7}/letify/providers/modal_adapter.py +0 -0
  89. {letify-2.6.5 → letify-2.6.7}/letify/providers/naming.py +0 -0
  90. {letify-2.6.5 → letify-2.6.7}/letify/providers/shell.py +0 -0
  91. {letify-2.6.5 → letify-2.6.7}/letify/providers/tunnel.py +0 -0
  92. {letify-2.6.5 → letify-2.6.7}/letify/providers/usage.py +0 -0
  93. {letify-2.6.5 → letify-2.6.7}/letify/remoting/__init__.py +0 -0
  94. {letify-2.6.5 → letify-2.6.7}/letify/remoting/capability.py +0 -0
  95. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/__init__.py +0 -0
  96. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/client.py +0 -0
  97. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/cuda.py +0 -0
  98. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/executor.py +0 -0
  99. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/frames.py +0 -0
  100. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/guard.py +0 -0
  101. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/tensor.py +0 -0
  102. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/trace.py +0 -0
  103. {letify-2.6.5 → letify-2.6.7}/letify/remoting/device/value.py +0 -0
  104. {letify-2.6.5 → letify-2.6.7}/letify/remoting/loader.py +0 -0
  105. {letify-2.6.5 → letify-2.6.7}/letify/remoting/probe.py +0 -0
  106. {letify-2.6.5 → letify-2.6.7}/letify/render.py +0 -0
  107. {letify-2.6.5 → letify-2.6.7}/letify/runtime/__init__.py +0 -0
  108. {letify-2.6.5 → letify-2.6.7}/letify/runtime/bootstrap.py +0 -0
  109. {letify-2.6.5 → letify-2.6.7}/letify/runtime/broker.py +0 -0
  110. {letify-2.6.5 → letify-2.6.7}/letify/runtime/channel.py +0 -0
  111. {letify-2.6.5 → letify-2.6.7}/letify/runtime/lease.py +0 -0
  112. {letify-2.6.5 → letify-2.6.7}/letify/runtime/pool.py +0 -0
  113. {letify-2.6.5 → letify-2.6.7}/letify/runtime/session.py +0 -0
  114. {letify-2.6.5 → letify-2.6.7}/letify/runtime/telemetry.py +0 -0
  115. {letify-2.6.5 → letify-2.6.7}/letify/store/__init__.py +0 -0
  116. {letify-2.6.5 → letify-2.6.7}/letify/store/backends/__init__.py +0 -0
  117. {letify-2.6.5 → letify-2.6.7}/letify/store/backends/filesystem.py +0 -0
  118. {letify-2.6.5 → letify-2.6.7}/letify/store/backends/google_auth.py +0 -0
  119. {letify-2.6.5 → letify-2.6.7}/letify/store/backends/layout.py +0 -0
  120. {letify-2.6.5 → letify-2.6.7}/letify/store/backends/objects.py +0 -0
  121. {letify-2.6.5 → letify-2.6.7}/letify/store/cas.py +0 -0
  122. {letify-2.6.5 → letify-2.6.7}/letify/store/sendorder.py +0 -0
  123. {letify-2.6.5 → letify-2.6.7}/letify/store/volume.py +0 -0
  124. {letify-2.6.5 → letify-2.6.7}/letify/stubs.py +0 -0
  125. {letify-2.6.5 → letify-2.6.7}/letify/tools.py +0 -0
  126. {letify-2.6.5 → letify-2.6.7}/letify/transport/__init__.py +0 -0
  127. {letify-2.6.5 → letify-2.6.7}/letify/transport/agent.py +0 -0
  128. {letify-2.6.5 → letify-2.6.7}/letify/transport/announce.py +0 -0
  129. {letify-2.6.5 → letify-2.6.7}/letify/transport/link.py +0 -0
  130. {letify-2.6.5 → letify-2.6.7}/letify/transport/nat.py +0 -0
  131. {letify-2.6.5 → letify-2.6.7}/letify/transport/pipeline.py +0 -0
  132. {letify-2.6.5 → letify-2.6.7}/letify/transport/portmap.py +0 -0
  133. {letify-2.6.5 → letify-2.6.7}/letify/transport/probe.py +0 -0
  134. {letify-2.6.5 → letify-2.6.7}/letify/transport/quic.py +0 -0
  135. {letify-2.6.5 → letify-2.6.7}/letify/transport/rendezvous.py +0 -0
  136. {letify-2.6.5 → letify-2.6.7}/letify/transport/setup.py +0 -0
  137. {letify-2.6.5 → letify-2.6.7}/letify/transport/sshopts.py +0 -0
  138. {letify-2.6.5 → letify-2.6.7}/letify/transport/strategies.py +0 -0
  139. {letify-2.6.5 → letify-2.6.7}/letify-core/.gitignore +0 -0
  140. {letify-2.6.5 → letify-2.6.7}/letify-core/build.py +0 -0
  141. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-agent/Cargo.toml +0 -0
  142. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-agent/src/driver.rs +0 -0
  143. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-agent/src/main.rs +0 -0
  144. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-driver/Cargo.toml +0 -0
  145. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-driver/src/client.rs +0 -0
  146. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-driver/src/exports.rs +0 -0
  147. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-driver/src/image.rs +0 -0
  148. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-driver/src/lib.rs +0 -0
  149. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-driver/src/status.rs +0 -0
  150. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-driver/src/table.rs +0 -0
  151. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-quic/Cargo.toml +0 -0
  152. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-quic/src/main.rs +0 -0
  153. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-wire/Cargo.toml +0 -0
  154. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-wire/src/lib.rs +0 -0
  155. {letify-2.6.5 → letify-2.6.7}/letify-core/letify-wire/tests/throughput.rs +0 -0
  156. {letify-2.6.5 → letify-2.6.7}/letify-core/rust-toolchain.toml +0 -0
  157. {letify-2.6.5 → letify-2.6.7}/letify-ext/.vscodeignore +0 -0
  158. {letify-2.6.5 → letify-2.6.7}/letify-ext/LICENSE +0 -0
  159. {letify-2.6.5 → letify-2.6.7}/letify-ext/README.md +0 -0
  160. {letify-2.6.5 → letify-2.6.7}/letify-ext/media/icon.svg +0 -0
  161. {letify-2.6.5 → letify-2.6.7}/letify-ext/src/cli.ts +0 -0
  162. {letify-2.6.5 → letify-2.6.7}/letify-ext/src/extension.ts +0 -0
  163. {letify-2.6.5 → letify-2.6.7}/letify-ext/src/history.ts +0 -0
  164. {letify-2.6.5 → letify-2.6.7}/letify-ext/src/model.ts +0 -0
  165. {letify-2.6.5 → letify-2.6.7}/letify-ext/src/poll.ts +0 -0
  166. {letify-2.6.5 → letify-2.6.7}/letify-ext/src/render.ts +0 -0
  167. {letify-2.6.5 → letify-2.6.7}/letify-ext/test/cli.test.ts +0 -0
  168. {letify-2.6.5 → letify-2.6.7}/letify-ext/test/manifest.test.ts +0 -0
  169. {letify-2.6.5 → letify-2.6.7}/letify-ext/test/model.test.ts +0 -0
  170. {letify-2.6.5 → letify-2.6.7}/letify-ext/test/regressions.test.ts +0 -0
  171. {letify-2.6.5 → letify-2.6.7}/letify-ext/tsconfig.json +0 -0
  172. {letify-2.6.5 → letify-2.6.7}/letify_providers.py +0 -0
  173. {letify-2.6.5 → letify-2.6.7}/scripts/sync-main.sh +0 -0
  174. {letify-2.6.5 → letify-2.6.7}/scripts/version.py +0 -0
  175. {letify-2.6.5 → letify-2.6.7}/tests/conftest.py +0 -0
  176. {letify-2.6.5 → letify-2.6.7}/tests/fake_kaggle_adapter.py +0 -0
  177. {letify-2.6.5 → letify-2.6.7}/tests/fake_modal_adapter.py +0 -0
  178. {letify-2.6.5 → letify-2.6.7}/tests/test_cli.py +0 -0
  179. {letify-2.6.5 → letify-2.6.7}/tests/test_cli_output.py +0 -0
  180. {letify-2.6.5 → letify-2.6.7}/tests/test_colab_keepalive.py +0 -0
  181. {letify-2.6.5 → letify-2.6.7}/tests/test_config.py +0 -0
  182. {letify-2.6.5 → letify-2.6.7}/tests/test_core.py +0 -0
  183. {letify-2.6.5 → letify-2.6.7}/tests/test_data.py +0 -0
  184. {letify-2.6.5 → letify-2.6.7}/tests/test_declare.py +0 -0
  185. {letify-2.6.5 → letify-2.6.7}/tests/test_device.py +0 -0
  186. {letify-2.6.5 → letify-2.6.7}/tests/test_distributed.py +0 -0
  187. {letify-2.6.5 → letify-2.6.7}/tests/test_editor_sessions.py +0 -0
  188. {letify-2.6.5 → letify-2.6.7}/tests/test_elice.py +0 -0
  189. {letify-2.6.5 → letify-2.6.7}/tests/test_frames.py +0 -0
  190. {letify-2.6.5 → letify-2.6.7}/tests/test_install.py +0 -0
  191. {letify-2.6.5 → letify-2.6.7}/tests/test_kaggle.py +0 -0
  192. {letify-2.6.5 → letify-2.6.7}/tests/test_kaggle_cookie.py +0 -0
  193. {letify-2.6.5 → letify-2.6.7}/tests/test_kaggle_link.py +0 -0
  194. {letify-2.6.5 → letify-2.6.7}/tests/test_link_log.py +0 -0
  195. {letify-2.6.5 → letify-2.6.7}/tests/test_notebook.py +0 -0
  196. {letify-2.6.5 → letify-2.6.7}/tests/test_parallel_placement.py +0 -0
  197. {letify-2.6.5 → letify-2.6.7}/tests/test_paths.py +0 -0
  198. {letify-2.6.5 → letify-2.6.7}/tests/test_portmap.py +0 -0
  199. {letify-2.6.5 → letify-2.6.7}/tests/test_protocol.py +0 -0
  200. {letify-2.6.5 → letify-2.6.7}/tests/test_quic.py +0 -0
  201. {letify-2.6.5 → letify-2.6.7}/tests/test_remoting.py +0 -0
  202. {letify-2.6.5 → letify-2.6.7}/tests/test_render.py +0 -0
  203. {letify-2.6.5 → letify-2.6.7}/tests/test_runtime.py +0 -0
  204. {letify-2.6.5 → letify-2.6.7}/tests/test_ship.py +0 -0
  205. {letify-2.6.5 → letify-2.6.7}/tests/test_store.py +0 -0
  206. {letify-2.6.5 → letify-2.6.7}/tests/test_strategies.py +0 -0
  207. {letify-2.6.5 → letify-2.6.7}/tests/test_stubs.py +0 -0
  208. {letify-2.6.5 → letify-2.6.7}/tests/test_transport.py +0 -0
  209. {letify-2.6.5 → letify-2.6.7}/tests/test_tunnel_setup.py +0 -0
  210. {letify-2.6.5 → letify-2.6.7}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.6.5
3
+ Version: 2.6.7
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
@@ -847,7 +847,20 @@ While the channel waits for `HELLO` each read waits at most 30 s, and a lane rea
847
847
 
848
848
  Socket buffers are left to the kernel. `SO_SNDBUF` and `SO_RCVBUF` are never set, because setting them turns off the kernel's buffer tuning and the value is capped at `net.core.wmem_max`, 212 KiB by default.
849
849
 
850
- When any step fails, a `tunnel` failure, a connection that does not open, or no `HELLO` within 30 s, the channel prints one line on stderr, `letify: <runtime>: the data channel did not open (<reason>); frames stay on standard input and output`, and keeps using standard input and output. The worker returns to reading standard input when a byte arrives there or its `wait` ends without an authenticated connection, so a request the channel sends over standard input after a failure is answered without waiting for `wait`.
850
+ The tunnel's host name is new when the sandbox is, so a resolver that is asked for it too early can
851
+ answer that it does not exist and keep that answer cached. Resolution is therefore retried for
852
+ `DATA_OPEN_WINDOW`, 30 s, every 3 s, before the channel gives up. Measured on a host where
853
+ `github.com` always resolved and a freshly created tunnel name did not, three sessions in a row fell
854
+ back, and 7.6 GB then went over standard input and output, where one piece held the control path long
855
+ enough for the lease renewal to miss its 60 s timeout and the worker pipe closed. Mornings on the
856
+ same host reached 45 to 90 MiB/s on the same path, so the fallback was the whole difference.
857
+
858
+ When the window ends without a connection, or a `tunnel` request fails, or no `HELLO` arrives within
859
+ 30 s, the channel prints one line on stderr, `letify: <runtime>: the data channel did not open
860
+ (<reason>, host <name>); frames stay on standard input and output, which the control path shares, so
861
+ a large transfer there can starve the lease`, and keeps using standard input and output. The host name
862
+ is named so a reader can resolve it themselves. The channel tries again on the next call rather than
863
+ staying that way, because the name may simply have been too new. The worker returns to reading standard input when a byte arrives there or its `wait` ends without an authenticated connection, so a request the channel sends over standard input after a failure is answered without waiting for `wait`.
851
864
 
852
865
  A `reexec` request goes over the data connection. The worker replies there and replaces its process, which closes the connection. The channel then sends the worker source over standard input, waits for `HELLO` there, and opens the data channel again.
853
866
 
@@ -1365,6 +1378,14 @@ A call that detected data prints one line on standard error:
1365
1378
 
1366
1379
  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.
1367
1380
 
1381
+ The bytes of a blob still on the way are reported too, because the rate counts only blobs that
1382
+ completed and a single large blob in flight therefore reads as `0.0 MiB/s` however fast it is moving.
1383
+ A line whose dropped clause names files ends with how far they got,
1384
+ `, 3 files 14985.8 MiB dropped when the call ended, 4512.0 MiB of them had arrived`, and the
1385
+ background sender draws the same `uploading` progress line the first wave draws, which it did not
1386
+ before: a blob sent beside the call reported nothing at all, so a transfer in progress and a transfer
1387
+ that had never started looked the same. Both cost a reader an afternoon.
1388
+
1368
1389
  `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.
1369
1390
 
1370
1391
  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.5"
69
+ __version__ = "2.6.7"
70
70
 
71
71
 
72
72
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -86,6 +86,13 @@ LISTEN_WAIT = 60
86
86
  #: Seconds the channel allows for connecting through the tunnel and for the hello after it.
87
87
  DATA_CONNECT_TIMEOUT = 30
88
88
 
89
+ #: Seconds the channel keeps trying to reach a tunnel whose host name does not resolve,
90
+ #: and the pause between attempts. Spec "Modal data channel": the name is new when the
91
+ #: sandbox is, so a resolver asked too early can answer that it does not exist and cache
92
+ #: that answer.
93
+ DATA_OPEN_WINDOW = 30.0
94
+ DATA_OPEN_PAUSE = 3.0
95
+
89
96
  #: TCP connections the data channel opens when ``data_streams`` is not set, and the most
90
97
  #: it accepts. Spec "Parallel data streams".
91
98
  DATA_STREAMS = 4
@@ -608,6 +615,23 @@ class TlsStream:
608
615
  self._wire.release()
609
616
 
610
617
 
618
+ def _reach(address: tuple[str, int]) -> socket.socket:
619
+ """Connect to the tunnel, waiting out a host name the resolver does not have yet.
620
+
621
+ Spec "Modal data channel": the name is new when the sandbox is, so a resolver asked
622
+ too early can answer that it does not exist and keep that answer cached. Anything but
623
+ a resolution failure is raised at once, because waiting does not help it.
624
+ """
625
+ deadline = time.monotonic() + DATA_OPEN_WINDOW
626
+ while True:
627
+ try:
628
+ return socket.create_connection(address, timeout=DATA_CONNECT_TIMEOUT)
629
+ except socket.gaierror:
630
+ if time.monotonic() >= deadline:
631
+ raise
632
+ time.sleep(DATA_OPEN_PAUSE)
633
+
634
+
611
635
  class SandboxChannel(FramedChannel):
612
636
  """Carries the worker's frames over a sandbox's pipes, through the adapter.
613
637
 
@@ -637,6 +661,9 @@ class SandboxChannel(FramedChannel):
637
661
  self.sandbox = sandbox
638
662
  self.name = name
639
663
  self.data_port = data_port
664
+ #: The tunnel host the last open reached for, named in a failure so a reader can
665
+ #: resolve it themselves. Spec "Modal data channel".
666
+ self._tunnel_host: str | None = None
640
667
  #: The sandbox's maximum lifetime, so a death at it is named rather than guessed.
641
668
  self.lifetime_s = lifetime_s
642
669
  self.opened_at = time.monotonic()
@@ -688,9 +715,11 @@ class SandboxChannel(FramedChannel):
688
715
  self._connection = self._open_data(self.data_port)
689
716
  except (LetifyError, OSError, ValueError, KeyError, TypeError) as exc:
690
717
  self._close_socket()
718
+ where = f", host {self._tunnel_host}" if self._tunnel_host else ""
691
719
  print(
692
- f"letify: {self.name}: the data channel did not open ({exc}); "
693
- "frames stay on standard input and output",
720
+ f"letify: {self.name}: the data channel did not open ({exc}{where}); "
721
+ "frames stay on standard input and output, which the control path "
722
+ "shares, so a large transfer there can starve the lease",
694
723
  file=sys.stderr,
695
724
  flush=True,
696
725
  )
@@ -707,6 +736,7 @@ class SandboxChannel(FramedChannel):
707
736
  stdio.request(listen, timeout=DATA_CONNECT_TIMEOUT + LISTEN_WAIT, kill=self._kill)
708
737
  tunnel = self.adapter.request("tunnel", sandbox=self.sandbox, port=port)
709
738
  host = str(tunnel["host"])
739
+ self._tunnel_host = host
710
740
  address = (host, int(tunnel["port"]))
711
741
  context = None
712
742
  if tunnel.get("tls"):
@@ -717,7 +747,7 @@ class SandboxChannel(FramedChannel):
717
747
  def open_lane(index: int) -> None:
718
748
  # Spec "Parallel data streams": every lane opens at the same time.
719
749
  try:
720
- raw = socket.create_connection(address, timeout=DATA_CONNECT_TIMEOUT)
750
+ raw = _reach(address)
721
751
  except BaseException as exc:
722
752
  errors.append(exc)
723
753
  return
@@ -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.5"
141
+ version = "2.6.7"
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.5"
149
+ version = "2.6.7"
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.5"
167
+ version = "2.6.7"
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.5"
11
+ version = "2.6.7"
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.5",
3
+ "version": "2.6.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.6.5",
9
+ "version": "2.6.7",
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.5",
5
+ "version": "2.6.7",
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.5"
3
+ version = "2.6.7"
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
@@ -2941,3 +2941,55 @@ def test_a_sandbox_that_reached_its_lifetime_says_so(config_file) -> None:
2941
2941
  assert "3600" in lifetime_detail(age_s=3599.5, lifetime_s=3600)
2942
2942
  assert "raise timeout" in lifetime_detail(age_s=3601.0, lifetime_s=3600)
2943
2943
  assert lifetime_detail(age_s=120.0, lifetime_s=3600) is None
2944
+
2945
+
2946
+ def test_a_tunnel_name_that_is_not_in_dns_yet_is_waited_for(
2947
+ isolated_home, fake_modal, monkeypatch
2948
+ ) -> None:
2949
+ """Spec "Modal data channel": the host name is new when the sandbox is.
2950
+
2951
+ KICT lost three sessions in a row to a resolver that answered NXDOMAIN for a freshly
2952
+ created tunnel name while github.com resolved every time, and 7.6 GB then went over
2953
+ standard input and output until the lease renewal missed its timeout.
2954
+ """
2955
+ import socket as socketmod
2956
+
2957
+ from letify.providers import modal as modalmod
2958
+
2959
+ attempts: list[float] = []
2960
+ real = socketmod.create_connection
2961
+
2962
+ def refuse_then_open(address, timeout=None, *args, **kwargs):
2963
+ attempts.append(0.0)
2964
+ if len(attempts) < 3:
2965
+ raise socketmod.gaierror(-3, "Temporary failure in name resolution")
2966
+ return real(address, timeout, *args, **kwargs)
2967
+
2968
+ monkeypatch.setattr(modalmod.socket, "create_connection", refuse_then_open)
2969
+ monkeypatch.setattr(modalmod, "DATA_OPEN_PAUSE", 0.01)
2970
+ provider = provider_of(Modal, "m")
2971
+ runtime = modal_runtime(provider)
2972
+ channel = provider.open_channel(runtime)
2973
+ try:
2974
+ channel.start()
2975
+ assert channel.call(sum, ([1, 2, 3],), {})[0] == 6
2976
+ assert len(attempts) >= 3, f"gave up after {len(attempts)} attempt(s)"
2977
+ finally:
2978
+ channel.close()
2979
+ provider.stop(runtime)
2980
+
2981
+
2982
+ def test_a_data_channel_failure_names_the_host(isolated_home, fake_modal, capfd) -> None:
2983
+ """Spec "Modal data channel": the host name is named so a reader can resolve it."""
2984
+ fake_modal.fail("tunnel")
2985
+ provider = provider_of(Modal, "m")
2986
+ runtime = modal_runtime(provider)
2987
+ channel = provider.open_channel(runtime)
2988
+ try:
2989
+ channel.start()
2990
+ assert channel.call(sum, ([1, 2, 3],), {})[0] == 6
2991
+ err = capfd.readouterr().err
2992
+ assert "the control path shares" in err, err
2993
+ finally:
2994
+ channel.close()
2995
+ provider.stop(runtime)
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