letify 2.10.1__tar.gz → 2.10.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. {letify-2.10.1 → letify-2.10.2}/PKG-INFO +1 -1
  2. {letify-2.10.1 → letify-2.10.2}/docs/SPEC.md +1 -1
  3. {letify-2.10.1 → letify-2.10.2}/letify/__init__.py +1 -1
  4. {letify-2.10.1 → letify-2.10.2}/letify/protocol/worker.py +12 -9
  5. {letify-2.10.1 → letify-2.10.2}/letify-core/Cargo.lock +2 -2
  6. {letify-2.10.1 → letify-2.10.2}/letify-core/Cargo.toml +1 -1
  7. {letify-2.10.1 → letify-2.10.2}/letify-ext/package-lock.json +2 -2
  8. {letify-2.10.1 → letify-2.10.2}/letify-ext/package.json +1 -1
  9. {letify-2.10.1 → letify-2.10.2}/pyproject.toml +1 -1
  10. {letify-2.10.1 → letify-2.10.2}/tests/test_run_takeover.py +110 -0
  11. {letify-2.10.1 → letify-2.10.2}/.github/workflows/ci.yml +0 -0
  12. {letify-2.10.1 → letify-2.10.2}/.github/workflows/docs.yml +0 -0
  13. {letify-2.10.1 → letify-2.10.2}/.github/workflows/publish-crates.yml +0 -0
  14. {letify-2.10.1 → letify-2.10.2}/.github/workflows/publish.yml +0 -0
  15. {letify-2.10.1 → letify-2.10.2}/.github/workflows/reserve-crates.yml +0 -0
  16. {letify-2.10.1 → letify-2.10.2}/.gitignore +0 -0
  17. {letify-2.10.1 → letify-2.10.2}/.researchtree +0 -0
  18. {letify-2.10.1 → letify-2.10.2}/CLAUDE.md +0 -0
  19. {letify-2.10.1 → letify-2.10.2}/LICENSE +0 -0
  20. {letify-2.10.1 → letify-2.10.2}/PROJECT.md +0 -0
  21. {letify-2.10.1 → letify-2.10.2}/README.md +0 -0
  22. {letify-2.10.1 → letify-2.10.2}/docs/COMPONENT.md +0 -0
  23. {letify-2.10.1 → letify-2.10.2}/docs/INTENT.md +0 -0
  24. {letify-2.10.1 → letify-2.10.2}/docs/NETWORK.md +0 -0
  25. {letify-2.10.1 → letify-2.10.2}/docs/guide/01-getting-started.md +0 -0
  26. {letify-2.10.1 → letify-2.10.2}/docs/guide/02-providers.md +0 -0
  27. {letify-2.10.1 → letify-2.10.2}/docs/guide/03-execution-modes.md +0 -0
  28. {letify-2.10.1 → letify-2.10.2}/docs/guide/04-environments-and-data.md +0 -0
  29. {letify-2.10.1 → letify-2.10.2}/docs/guide/05-concurrency.md +0 -0
  30. {letify-2.10.1 → letify-2.10.2}/docs/guide/06-cost.md +0 -0
  31. {letify-2.10.1 → letify-2.10.2}/docs/guide/07-troubleshooting.md +0 -0
  32. {letify-2.10.1 → letify-2.10.2}/docs/guide/08-releasing.md +0 -0
  33. {letify-2.10.1 → letify-2.10.2}/docs/guide/README.md +0 -0
  34. {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/01-/354/213/234/354/236/221/355/225/230/352/270/260.md" +0 -0
  35. {letify-2.10.1 → letify-2.10.2}/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.10.1 → letify-2.10.2}/docs/guide/ko/03-/354/213/244/355/226/211-/353/260/251/354/213/235.md" +0 -0
  37. {letify-2.10.1 → letify-2.10.2}/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.10.1 → letify-2.10.2}/docs/guide/ko/05-/353/217/231/354/213/234-/354/213/244/355/226/211.md" +0 -0
  39. {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/06-/353/271/204/354/232/251.md" +0 -0
  40. {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/07-/353/254/270/354/240/234-/355/225/264/352/262/260.md" +0 -0
  41. {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/08-/353/246/264/353/246/254/354/212/244.md" +0 -0
  42. {letify-2.10.1 → letify-2.10.2}/docs/guide/ko/README.md +0 -0
  43. {letify-2.10.1 → letify-2.10.2}/docs/locales/README_ko.md +0 -0
  44. {letify-2.10.1 → letify-2.10.2}/docs/mkdocs.yml +0 -0
  45. {letify-2.10.1 → letify-2.10.2}/examples/README.md +0 -0
  46. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/.letify.example +0 -0
  47. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/00_smoke.py +0 -0
  48. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/01_sweep.py +0 -0
  49. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/02_resume.py +0 -0
  50. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/03_watch.py +0 -0
  51. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/README.md +0 -0
  52. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/README_ko.md +0 -0
  53. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/common.py +0 -0
  54. {letify-2.10.1 → letify-2.10.2}/examples/nvfp4-lora/recipe.py +0 -0
  55. {letify-2.10.1 → letify-2.10.2}/letify/cli.py +0 -0
  56. {letify-2.10.1 → letify-2.10.2}/letify/config/__init__.py +0 -0
  57. {letify-2.10.1 → letify-2.10.2}/letify/config/inventory.py +0 -0
  58. {letify-2.10.1 → letify-2.10.2}/letify/config/login.py +0 -0
  59. {letify-2.10.1 → letify-2.10.2}/letify/config/schema.py +0 -0
  60. {letify-2.10.1 → letify-2.10.2}/letify/config/secrets.py +0 -0
  61. {letify-2.10.1 → letify-2.10.2}/letify/config/writer.py +0 -0
  62. {letify-2.10.1 → letify-2.10.2}/letify/declare/__init__.py +0 -0
  63. {letify-2.10.1 → letify-2.10.2}/letify/declare/cache.py +0 -0
  64. {letify-2.10.1 → letify-2.10.2}/letify/declare/env.py +0 -0
  65. {letify-2.10.1 → letify-2.10.2}/letify/declare/exit.py +0 -0
  66. {letify-2.10.1 → letify-2.10.2}/letify/declare/function.py +0 -0
  67. {letify-2.10.1 → letify-2.10.2}/letify/declare/instance.py +0 -0
  68. {letify-2.10.1 → letify-2.10.2}/letify/declare/reduce.py +0 -0
  69. {letify-2.10.1 → letify-2.10.2}/letify/declare/replicas.py +0 -0
  70. {letify-2.10.1 → letify-2.10.2}/letify/errors.py +0 -0
  71. {letify-2.10.1 → letify-2.10.2}/letify/install.py +0 -0
  72. {letify-2.10.1 → letify-2.10.2}/letify/launcher.py +0 -0
  73. {letify-2.10.1 → letify-2.10.2}/letify/paths.py +0 -0
  74. {letify-2.10.1 → letify-2.10.2}/letify/protocol/__init__.py +0 -0
  75. {letify-2.10.1 → letify-2.10.2}/letify/protocol/codec.py +0 -0
  76. {letify-2.10.1 → letify-2.10.2}/letify/protocol/driver.py +0 -0
  77. {letify-2.10.1 → letify-2.10.2}/letify/protocol/handle.py +0 -0
  78. {letify-2.10.1 → letify-2.10.2}/letify/protocol/wire.py +0 -0
  79. {letify-2.10.1 → letify-2.10.2}/letify/providers/__init__.py +0 -0
  80. {letify-2.10.1 → letify-2.10.2}/letify/providers/base.py +0 -0
  81. {letify-2.10.1 → letify-2.10.2}/letify/providers/colab.py +0 -0
  82. {letify-2.10.1 → letify-2.10.2}/letify/providers/colab_files.py +0 -0
  83. {letify-2.10.1 → letify-2.10.2}/letify/providers/colab_keepalive.py +0 -0
  84. {letify-2.10.1 → letify-2.10.2}/letify/providers/elice.py +0 -0
  85. {letify-2.10.1 → letify-2.10.2}/letify/providers/kaggle.py +0 -0
  86. {letify-2.10.1 → letify-2.10.2}/letify/providers/kaggle_adapter.py +0 -0
  87. {letify-2.10.1 → letify-2.10.2}/letify/providers/local.py +0 -0
  88. {letify-2.10.1 → letify-2.10.2}/letify/providers/modal.py +0 -0
  89. {letify-2.10.1 → letify-2.10.2}/letify/providers/modal_adapter.py +0 -0
  90. {letify-2.10.1 → letify-2.10.2}/letify/providers/naming.py +0 -0
  91. {letify-2.10.1 → letify-2.10.2}/letify/providers/shell.py +0 -0
  92. {letify-2.10.1 → letify-2.10.2}/letify/providers/tunnel.py +0 -0
  93. {letify-2.10.1 → letify-2.10.2}/letify/providers/usage.py +0 -0
  94. {letify-2.10.1 → letify-2.10.2}/letify/remoting/__init__.py +0 -0
  95. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/__init__.py +0 -0
  96. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/client.py +0 -0
  97. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/cuda.py +0 -0
  98. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/executor.py +0 -0
  99. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/frames.py +0 -0
  100. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/guard.py +0 -0
  101. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/tensor.py +0 -0
  102. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/trace.py +0 -0
  103. {letify-2.10.1 → letify-2.10.2}/letify/remoting/device/value.py +0 -0
  104. {letify-2.10.1 → letify-2.10.2}/letify/render.py +0 -0
  105. {letify-2.10.1 → letify-2.10.2}/letify/runtime/__init__.py +0 -0
  106. {letify-2.10.1 → letify-2.10.2}/letify/runtime/bootstrap.py +0 -0
  107. {letify-2.10.1 → letify-2.10.2}/letify/runtime/broker.py +0 -0
  108. {letify-2.10.1 → letify-2.10.2}/letify/runtime/channel.py +0 -0
  109. {letify-2.10.1 → letify-2.10.2}/letify/runtime/control.py +0 -0
  110. {letify-2.10.1 → letify-2.10.2}/letify/runtime/lease.py +0 -0
  111. {letify-2.10.1 → letify-2.10.2}/letify/runtime/pool.py +0 -0
  112. {letify-2.10.1 → letify-2.10.2}/letify/runtime/session.py +0 -0
  113. {letify-2.10.1 → letify-2.10.2}/letify/runtime/takeover.py +0 -0
  114. {letify-2.10.1 → letify-2.10.2}/letify/runtime/telemetry.py +0 -0
  115. {letify-2.10.1 → letify-2.10.2}/letify/runtime/workers.py +0 -0
  116. {letify-2.10.1 → letify-2.10.2}/letify/store/__init__.py +0 -0
  117. {letify-2.10.1 → letify-2.10.2}/letify/store/backends/__init__.py +0 -0
  118. {letify-2.10.1 → letify-2.10.2}/letify/store/backends/drive.py +0 -0
  119. {letify-2.10.1 → letify-2.10.2}/letify/store/backends/filesystem.py +0 -0
  120. {letify-2.10.1 → letify-2.10.2}/letify/store/backends/google_auth.py +0 -0
  121. {letify-2.10.1 → letify-2.10.2}/letify/store/backends/layout.py +0 -0
  122. {letify-2.10.1 → letify-2.10.2}/letify/store/backends/objects.py +0 -0
  123. {letify-2.10.1 → letify-2.10.2}/letify/store/cas.py +0 -0
  124. {letify-2.10.1 → letify-2.10.2}/letify/store/handoff.py +0 -0
  125. {letify-2.10.1 → letify-2.10.2}/letify/store/pathdata.py +0 -0
  126. {letify-2.10.1 → letify-2.10.2}/letify/store/sendorder.py +0 -0
  127. {letify-2.10.1 → letify-2.10.2}/letify/store/volume.py +0 -0
  128. {letify-2.10.1 → letify-2.10.2}/letify/stubs.py +0 -0
  129. {letify-2.10.1 → letify-2.10.2}/letify/tools.py +0 -0
  130. {letify-2.10.1 → letify-2.10.2}/letify/transport/__init__.py +0 -0
  131. {letify-2.10.1 → letify-2.10.2}/letify/transport/agent.py +0 -0
  132. {letify-2.10.1 → letify-2.10.2}/letify/transport/announce.py +0 -0
  133. {letify-2.10.1 → letify-2.10.2}/letify/transport/link.py +0 -0
  134. {letify-2.10.1 → letify-2.10.2}/letify/transport/nat.py +0 -0
  135. {letify-2.10.1 → letify-2.10.2}/letify/transport/pipeline.py +0 -0
  136. {letify-2.10.1 → letify-2.10.2}/letify/transport/portmap.py +0 -0
  137. {letify-2.10.1 → letify-2.10.2}/letify/transport/probe.py +0 -0
  138. {letify-2.10.1 → letify-2.10.2}/letify/transport/quic.py +0 -0
  139. {letify-2.10.1 → letify-2.10.2}/letify/transport/rendezvous.py +0 -0
  140. {letify-2.10.1 → letify-2.10.2}/letify/transport/setup.py +0 -0
  141. {letify-2.10.1 → letify-2.10.2}/letify/transport/sshopts.py +0 -0
  142. {letify-2.10.1 → letify-2.10.2}/letify/transport/strategies.py +0 -0
  143. {letify-2.10.1 → letify-2.10.2}/letify-core/.gitignore +0 -0
  144. {letify-2.10.1 → letify-2.10.2}/letify-core/build.py +0 -0
  145. {letify-2.10.1 → letify-2.10.2}/letify-core/letify/Cargo.toml +0 -0
  146. {letify-2.10.1 → letify-2.10.2}/letify-core/letify/LICENSE +0 -0
  147. {letify-2.10.1 → letify-2.10.2}/letify-core/letify/README.md +0 -0
  148. {letify-2.10.1 → letify-2.10.2}/letify-core/letify/src/lib.rs +0 -0
  149. {letify-2.10.1 → letify-2.10.2}/letify-core/letify-quic/Cargo.toml +0 -0
  150. {letify-2.10.1 → letify-2.10.2}/letify-core/letify-quic/src/main.rs +0 -0
  151. {letify-2.10.1 → letify-2.10.2}/letify-core/rust-toolchain.toml +0 -0
  152. {letify-2.10.1 → letify-2.10.2}/letify-ext/.vscodeignore +0 -0
  153. {letify-2.10.1 → letify-2.10.2}/letify-ext/LICENSE +0 -0
  154. {letify-2.10.1 → letify-2.10.2}/letify-ext/README.md +0 -0
  155. {letify-2.10.1 → letify-2.10.2}/letify-ext/media/icon.svg +0 -0
  156. {letify-2.10.1 → letify-2.10.2}/letify-ext/src/cli.ts +0 -0
  157. {letify-2.10.1 → letify-2.10.2}/letify-ext/src/extension.ts +0 -0
  158. {letify-2.10.1 → letify-2.10.2}/letify-ext/src/history.ts +0 -0
  159. {letify-2.10.1 → letify-2.10.2}/letify-ext/src/model.ts +0 -0
  160. {letify-2.10.1 → letify-2.10.2}/letify-ext/src/poll.ts +0 -0
  161. {letify-2.10.1 → letify-2.10.2}/letify-ext/src/render.ts +0 -0
  162. {letify-2.10.1 → letify-2.10.2}/letify-ext/test/cli.test.ts +0 -0
  163. {letify-2.10.1 → letify-2.10.2}/letify-ext/test/manifest.test.ts +0 -0
  164. {letify-2.10.1 → letify-2.10.2}/letify-ext/test/model.test.ts +0 -0
  165. {letify-2.10.1 → letify-2.10.2}/letify-ext/test/regressions.test.ts +0 -0
  166. {letify-2.10.1 → letify-2.10.2}/letify-ext/tsconfig.json +0 -0
  167. {letify-2.10.1 → letify-2.10.2}/letify_providers.py +0 -0
  168. {letify-2.10.1 → letify-2.10.2}/scripts/sync-main.sh +0 -0
  169. {letify-2.10.1 → letify-2.10.2}/scripts/version.py +0 -0
  170. {letify-2.10.1 → letify-2.10.2}/tests/conftest.py +0 -0
  171. {letify-2.10.1 → letify-2.10.2}/tests/fake_kaggle_adapter.py +0 -0
  172. {letify-2.10.1 → letify-2.10.2}/tests/fake_modal_adapter.py +0 -0
  173. {letify-2.10.1 → letify-2.10.2}/tests/test_cli.py +0 -0
  174. {letify-2.10.1 → letify-2.10.2}/tests/test_cli_output.py +0 -0
  175. {letify-2.10.1 → letify-2.10.2}/tests/test_colab_keepalive.py +0 -0
  176. {letify-2.10.1 → letify-2.10.2}/tests/test_config.py +0 -0
  177. {letify-2.10.1 → letify-2.10.2}/tests/test_core.py +0 -0
  178. {letify-2.10.1 → letify-2.10.2}/tests/test_data.py +0 -0
  179. {letify-2.10.1 → letify-2.10.2}/tests/test_data_streaming.py +0 -0
  180. {letify-2.10.1 → letify-2.10.2}/tests/test_declare.py +0 -0
  181. {letify-2.10.1 → letify-2.10.2}/tests/test_device.py +0 -0
  182. {letify-2.10.1 → letify-2.10.2}/tests/test_device_compile.py +0 -0
  183. {letify-2.10.1 → letify-2.10.2}/tests/test_distributed.py +0 -0
  184. {letify-2.10.1 → letify-2.10.2}/tests/test_drive_handoff.py +0 -0
  185. {letify-2.10.1 → letify-2.10.2}/tests/test_drive_store.py +0 -0
  186. {letify-2.10.1 → letify-2.10.2}/tests/test_editor_sessions.py +0 -0
  187. {letify-2.10.1 → letify-2.10.2}/tests/test_elice.py +0 -0
  188. {letify-2.10.1 → letify-2.10.2}/tests/test_frames.py +0 -0
  189. {letify-2.10.1 → letify-2.10.2}/tests/test_install.py +0 -0
  190. {letify-2.10.1 → letify-2.10.2}/tests/test_kaggle.py +0 -0
  191. {letify-2.10.1 → letify-2.10.2}/tests/test_kaggle_cookie.py +0 -0
  192. {letify-2.10.1 → letify-2.10.2}/tests/test_kaggle_link.py +0 -0
  193. {letify-2.10.1 → letify-2.10.2}/tests/test_link_log.py +0 -0
  194. {letify-2.10.1 → letify-2.10.2}/tests/test_notebook.py +0 -0
  195. {letify-2.10.1 → letify-2.10.2}/tests/test_parallel_placement.py +0 -0
  196. {letify-2.10.1 → letify-2.10.2}/tests/test_paths.py +0 -0
  197. {letify-2.10.1 → letify-2.10.2}/tests/test_portmap.py +0 -0
  198. {letify-2.10.1 → letify-2.10.2}/tests/test_protocol.py +0 -0
  199. {letify-2.10.1 → letify-2.10.2}/tests/test_providers.py +0 -0
  200. {letify-2.10.1 → letify-2.10.2}/tests/test_punch_sockets.py +0 -0
  201. {letify-2.10.1 → letify-2.10.2}/tests/test_quic.py +0 -0
  202. {letify-2.10.1 → letify-2.10.2}/tests/test_remoting.py +0 -0
  203. {letify-2.10.1 → letify-2.10.2}/tests/test_render.py +0 -0
  204. {letify-2.10.1 → letify-2.10.2}/tests/test_run_control.py +0 -0
  205. {letify-2.10.1 → letify-2.10.2}/tests/test_runtime.py +0 -0
  206. {letify-2.10.1 → letify-2.10.2}/tests/test_shell_workers.py +0 -0
  207. {letify-2.10.1 → letify-2.10.2}/tests/test_ship.py +0 -0
  208. {letify-2.10.1 → letify-2.10.2}/tests/test_store.py +0 -0
  209. {letify-2.10.1 → letify-2.10.2}/tests/test_strategies.py +0 -0
  210. {letify-2.10.1 → letify-2.10.2}/tests/test_stubs.py +0 -0
  211. {letify-2.10.1 → letify-2.10.2}/tests/test_transport.py +0 -0
  212. {letify-2.10.1 → letify-2.10.2}/tests/test_tunnel_setup.py +0 -0
  213. {letify-2.10.1 → letify-2.10.2}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: letify
3
- Version: 2.10.1
3
+ Version: 2.10.2
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
@@ -1089,7 +1089,7 @@ A call has exactly one owner at a time, and the session still ends with the call
1089
1089
 
1090
1090
  1. **The registry keeps an orphan.** The record also holds `project`, the project root the call was sent from, and `lease_deadline`, the Unix time at which the runtime's lease ends unless renewed, rewritten at each renewal. It also holds what a second process needs to reach the call and finish it: `door`, the path of the worker's socket below; `config`, the project `config.toml` that names the account; and `call_dir`, `blobs` and `outputs`, the runtime paths and the output locations to write back, each location stored relative to `project` when it lies inside it. No secret is among them. A record whose owner pid is gone is no longer removed when read while its `lease_deadline` is in the future: `letify runs` lists it with `owner` `gone` and the seconds left, and it can be taken over. After the deadline it is removed as stale.
1091
1091
  2. **The worker keeps a second door.** While a call runs, the worker listens on a Unix socket on the runtime, `<workspace root>/.letify/run-<run id>.sock`, mode 0600. When that path is longer than 100 bytes, the limit of a Unix socket address, the worker and the relay reach the socket through `/proc/self/fd/<n>`, a descriptor of its directory, so it stays at that path; where `/proc` is missing the worker reports that the run has no second door. A channel that closes while a call runs, with no shutdown frame before it, does not end the worker: the body keeps running until the lease deadline passes. A shutdown frame is different: it is the owner ending the run on purpose, as [A deliberate shutdown](#run-shutdown) describes. Output the body makes while no channel is open is kept, the last 256 KiB per stream, and is sent to the next owner first. A body that returns while no channel is open keeps its result for a new owner until the lease deadline; on a provider with no lease the worker ends then, as before.
1092
- 3. **The new owner connects.** `letify takeover <run>` reads the record, opens a channel to the same runtime through the record's provider and runtime name, and starts a relay to that socket instead of a new worker. It then sends `adopt` with the run id. The worker answers once with the call's state, which is `running` or `finished`, and from then on sends the call's output and its reply on the new channel, the reply as the last piece of the same answer. The old channel, if still open, is told `moved` on the call's stream and answers every later request on it with `moved`. A door takes one connection at a time and no request but `adopt` before it, and a second owner can adopt after the first one's connection has closed.
1092
+ 3. **The new owner connects.** `letify takeover <run>` reads the record, opens a channel to the same runtime through the record's provider and runtime name, and starts a relay to that socket instead of a new worker. It then sends `adopt` with the run id. The worker answers once with the call's state, which is `running` or `finished`, and from then on sends the call's output and its reply on the new channel, the reply as the last piece of the same answer. The old channel, if still open, is told `moved` on the call's stream, after the worker has switched the call's output to the new channel, so everything the body prints once the old owner knows of the move reaches the new owner. It answers every later request on it with `moved`. A door takes one connection at a time and no request but `adopt` before it, and a second owner can adopt after the first one's connection has closed. The adoption is atomic with respect to the old channel: if the old channel closes while the adoption is under way, the worker decides what that close means only after the adoption has finished, sees that the call has moved, and does nothing. The new owner's channel is then never marked as having lost its input, and no exit hook runs for the close.
1093
1093
  4. **The old owner lets go.** When the old owner is alive, takeover first asks it through its control socket to release the run. Its waiting call raises `OwnershipMoved` naming the run and the new owner's pid, it stops renewing that runtime's lease, and it drops the runtime from its pool without stopping it. A `keep_alive` block in the old owner no longer holds that session. An old owner that does not answer within 5 s is treated as gone.
1094
1094
  5. **The call ends at the new owner.** `letify takeover` prints the body's output as it arrives. When the call returns it prints the `repr` of the value, or with `--result PATH` writes the value with pickle to `PATH`, and exits 0. When the body raises it prints the traceback and exits 1. With `--json` the last line is one object with `run`, `ok`, `value_repr` or `error`, and `result_path`. The call's outputs are written back under the record's `project`, not the current directory. The session is then ended.
1095
1095
  6. **The same from Python.** `letify.takeover(run)` does the same in the calling process and returns the value or raises the body's exception, as a call does. It holds no handle: it returns once the call has ended and the session is gone. A darkpyonix kernel uses this to adopt a run started elsewhere.
@@ -70,7 +70,7 @@ distributed: Final = Host.distributed
70
70
  # The enum class stays importable from letify.declare.instance for letify's own use.
71
71
  del Host
72
72
 
73
- __version__ = "2.10.1"
73
+ __version__ = "2.10.2"
74
74
 
75
75
 
76
76
  def fetch(tensor): # type: ignore[no-untyped-def]
@@ -3549,11 +3549,12 @@ def _reader_ended(door, shutdown=False):
3549
3549
  Spec "A deliberate shutdown": a shutdown frame on the channel that owns a running call
3550
3550
  ends the body and the worker. End of input alone leaves the body for a takeover.
3551
3551
  """
3552
- if door is None and _TAKE["moved"]:
3553
- # The call was adopted away; this channel closing is the old owner leaving.
3554
- return
3555
3552
  during = False
3556
3553
  with _TAKE_COND:
3554
+ if door is None and _TAKE["moved"]:
3555
+ # The call was adopted away; this channel closing is the old owner leaving. Read
3556
+ # under the lock, because an adoption in progress holds it until it has moved.
3557
+ return
3557
3558
  if door is None or (_TAKE["owner"] is door and _TAKE["live"]):
3558
3559
  during = _TAKE["run"] is not None
3559
3560
  if door is not None or during:
@@ -3702,12 +3703,6 @@ def _door_adopt(door, stream, request):
3702
3703
  moved = _moved_outcome()
3703
3704
  if _TAKE["owner"] is not None:
3704
3705
  moved["last"] = True
3705
- # Told on a thread, because a former owner that has stopped reading can block the write.
3706
- told = threading.Thread(
3707
- target=_answer, args=(former, former_stream, moved), daemon=True
3708
- )
3709
- told.start()
3710
- told.join(2.0)
3711
3706
  state = {
3712
3707
  "run": run,
3713
3708
  "state": "finished" if _TAKE["finished"] else "running",
@@ -3728,6 +3723,14 @@ def _door_adopt(door, stream, request):
3728
3723
  )
3729
3724
  _SENDER = door.sender
3730
3725
  _TAKE_COND.notify_all()
3726
+ # Told last, after the output has been switched, so that nothing the body prints from
3727
+ # the moment the former owner learns of the move is sent to a channel that has stopped
3728
+ # reading. A thread, because a former owner that has stopped reading can block the write.
3729
+ told = threading.Thread(
3730
+ target=_answer, args=(former, former_stream, moved), daemon=True
3731
+ )
3732
+ told.start()
3733
+ told.join(2.0)
3731
3734
 
3732
3735
 
3733
3736
  def _deliver_call(stream, outcome):
@@ -138,11 +138,11 @@ dependencies = [
138
138
 
139
139
  [[package]]
140
140
  name = "letify"
141
- version = "2.10.1"
141
+ version = "2.10.2"
142
142
 
143
143
  [[package]]
144
144
  name = "letify-quic"
145
- version = "2.10.1"
145
+ version = "2.10.2"
146
146
  dependencies = [
147
147
  "quinn",
148
148
  "rcgen",
@@ -8,7 +8,7 @@ resolver = "3"
8
8
  members = ["letify", "letify-quic"]
9
9
 
10
10
  [workspace.package]
11
- version = "2.10.1"
11
+ version = "2.10.2"
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.10.1",
3
+ "version": "2.10.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "letify-status",
9
- "version": "2.10.1",
9
+ "version": "2.10.2",
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.10.1",
5
+ "version": "2.10.2",
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.10.1"
3
+ version = "2.10.2"
4
4
  description = "Declarations that become infrastructure."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -289,6 +289,8 @@ def test_the_new_owner_takes_the_record_and_removes_it_when_the_call_ends(world,
289
289
  until(lambda: any(r["pid"] == os.getpid() for r in control.list_runs()), "the new record")
290
290
  row = next(r for r in control.list_runs() if r["pid"] == os.getpid())
291
291
  assert row["run"] == owner.run
292
+ # The body returns only once the old owner has let go, which is after the adoption.
293
+ owner.answer()
292
294
  owner.finish_body()
293
295
  taking.join()
294
296
  assert control.list_runs(orphans=True) == []
@@ -742,3 +744,111 @@ def test_detaching_a_persistent_channel_sends_no_shutdown(monkeypatch) -> None:
742
744
  channel.detach()
743
745
  assert wire.SHUTDOWN not in sent
744
746
  assert sent == []
747
+
748
+
749
+ #: The old owner's channel closes while the worker is still adopting the call to the new owner.
750
+ #: The adoption holds the call's lock, so the close has to wait for it and then see the move.
751
+ RACE_PROBE = textwrap.dedent(
752
+ """
753
+ import json, sys, threading, time
754
+ from pathlib import Path
755
+
756
+ from letify.protocol import worker
757
+
758
+ out = sys.argv[1]
759
+ text = worker.SOURCE.rstrip()
760
+ assert text.endswith("_serve()")
761
+ space = {"__name__": "probe"}
762
+ exec(compile(text[: -len("_serve()")], "worker", "exec"), space)
763
+ hooks = []
764
+ space["_run_exit_hooks"] = lambda reason: hooks.append(reason)
765
+ take, cond = space["_TAKE"], space["_TAKE_COND"]
766
+ take.update(run="abcdef012345", live=True, moved=False)
767
+ new_owner = object()
768
+ with cond:
769
+ closing = threading.Thread(target=space["_reader_ended"], args=(None, False))
770
+ closing.start()
771
+ time.sleep(0.5)
772
+ # The adoption finishes under the lock, after the close began.
773
+ take.update(owner=new_owner, moved=True, live=True, ended_input=False)
774
+ closing.join(30)
775
+ Path(out).write_text(json.dumps({
776
+ "live": take["live"], "ended_input": take["ended_input"], "hooks": hooks,
777
+ }))
778
+ """
779
+ )
780
+
781
+
782
+ def test_the_old_owners_channel_closing_during_the_adoption_leaves_the_new_owner_live(
783
+ tmp_path,
784
+ ) -> None:
785
+ script = tmp_path / "race.py"
786
+ script.write_text(RACE_PROBE, encoding="utf-8")
787
+ out = tmp_path / "race.out"
788
+ root = str(Path(letify.__file__).resolve().parent.parent)
789
+ subprocess.run(
790
+ [sys.executable, str(script), str(out)],
791
+ check=True, timeout=60, env={**os.environ, "PYTHONPATH": root},
792
+ stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
793
+ )
794
+ assert json.loads(out.read_text(encoding="utf-8")) == {
795
+ "live": True, "ended_input": False, "hooks": [],
796
+ }
797
+
798
+
799
+ #: Adopts a call with stand-in channels and records whether the worker already sends output
800
+ #: to the new owner at the moment the old owner is told the call has moved.
801
+ ORDER_PROBE = textwrap.dedent(
802
+ """
803
+ import json, sys
804
+ from pathlib import Path
805
+
806
+ from letify.protocol import worker
807
+
808
+ out = sys.argv[1]
809
+ text = worker.SOURCE.rstrip()
810
+ assert text.endswith("_serve()")
811
+ space = {"__name__": "probe"}
812
+ exec(compile(text[: -len("_serve()")], "worker", "exec"), space)
813
+
814
+
815
+ class Channel:
816
+ def message(self, *args):
817
+ pass
818
+
819
+ def frame(self, *args):
820
+ pass
821
+
822
+
823
+ class Connection:
824
+ def settimeout(self, value):
825
+ pass
826
+
827
+
828
+ class Door:
829
+ sender = Channel()
830
+ connection = Connection()
831
+
832
+
833
+ door, former = Door(), Channel()
834
+ seen = []
835
+ space["_answer"] = lambda sender, stream, outcome: seen.append(space["_SENDER"] is door.sender)
836
+ space["_SENDER"] = former
837
+ space["_TAKE"].update(run="abcdef012345", live=True, stream=3)
838
+ space["_door_adopt"](door, 7, {"run": "abcdef012345", "owner": 1234})
839
+ Path(out).write_text(json.dumps(seen))
840
+ """
841
+ )
842
+
843
+
844
+ def test_output_after_the_old_owner_is_told_the_call_moved_goes_to_the_new_owner(tmp_path) -> None:
845
+ script = tmp_path / "order.py"
846
+ script.write_text(ORDER_PROBE, encoding="utf-8")
847
+ out = tmp_path / "order.out"
848
+ root = str(Path(letify.__file__).resolve().parent.parent)
849
+ subprocess.run(
850
+ [sys.executable, str(script), str(out)],
851
+ check=True, timeout=60, env={**os.environ, "PYTHONPATH": root},
852
+ stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
853
+ )
854
+ assert json.loads(out.read_text(encoding="utf-8")) == [True]
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