tensorfs 0.3.45__tar.gz → 0.3.47__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 (137) hide show
  1. {tensorfs-0.3.45 → tensorfs-0.3.47}/Cargo.lock +2 -2
  2. {tensorfs-0.3.45 → tensorfs-0.3.47}/Cargo.toml +1 -1
  3. {tensorfs-0.3.45 → tensorfs-0.3.47}/PKG-INFO +1 -1
  4. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/Cargo.toml +1 -1
  5. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/asset_update.rs +111 -0
  6. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/http.rs +84 -14
  7. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/ledger.rs +20 -0
  8. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/pull.rs +8 -2
  9. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/tests.rs +331 -0
  10. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-py/src/store.rs +32 -3
  11. {tensorfs-0.3.45 → tensorfs-0.3.47}/pyproject.toml +1 -1
  12. {tensorfs-0.3.45 → tensorfs-0.3.47}/python/tensorfs/_ext.pyi +9 -0
  13. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/build.rs +0 -0
  14. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/examples/h3-swiglu-layout.md +0 -0
  15. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/examples/optional-cache-backfill.md +0 -0
  16. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/examples/resident-conversion.md +0 -0
  17. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/examples/resident_conversion_profile.rs +0 -0
  18. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/b64.rs +0 -0
  19. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/ckpt.rs +0 -0
  20. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/conform.rs +0 -0
  21. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/fetch.rs +0 -0
  22. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/ingest.rs +0 -0
  23. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/main.rs +0 -0
  24. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/rd.rs +0 -0
  25. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/repo.rs +0 -0
  26. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/snap.rs +0 -0
  27. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/bin/tfs/watch.rs +0 -0
  28. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/cache_roots.rs +0 -0
  29. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/canon.rs +0 -0
  30. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/capability.rs +0 -0
  31. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/catalog.rs +0 -0
  32. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/cbor.rs +0 -0
  33. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/checkpoint.rs +0 -0
  34. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/checkpoint_root.rs +0 -0
  35. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/corpus.rs +0 -0
  36. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/derived/adoption.rs +0 -0
  37. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/derived/estimate.rs +0 -0
  38. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/derived/graft_tests.rs +0 -0
  39. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/derived/progress.rs +0 -0
  40. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/derived/resume_tests.rs +0 -0
  41. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/derived/retention.rs +0 -0
  42. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/derived/roots.rs +0 -0
  43. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/derived.rs +0 -0
  44. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/dtype.rs +0 -0
  45. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/durability.rs +0 -0
  46. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/err.rs +0 -0
  47. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/fetch.rs +0 -0
  48. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/fill.rs +0 -0
  49. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/fit.rs +0 -0
  50. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/gc.rs +0 -0
  51. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/header.rs +0 -0
  52. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/home.rs +0 -0
  53. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ids.rs +0 -0
  54. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/carrier.rs +0 -0
  55. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/convert.rs +0 -0
  56. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/fingerprint.rs +0 -0
  57. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/gguf.rs +0 -0
  58. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/h3_lora.rs +0 -0
  59. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/journal.rs +0 -0
  60. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/mod.rs +0 -0
  61. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/preflight.rs +0 -0
  62. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/source.rs +0 -0
  63. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/stamp.rs +0 -0
  64. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/ingest/transaction.rs +0 -0
  65. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/jcs.rs +0 -0
  66. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/lib.rs +0 -0
  67. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/limits.rs +0 -0
  68. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/machine.rs +0 -0
  69. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/manifest.rs +0 -0
  70. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/meta.rs +0 -0
  71. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/project.rs +0 -0
  72. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/providers.rs +0 -0
  73. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/read.rs +0 -0
  74. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/receipt.rs +0 -0
  75. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/reclaim.rs +0 -0
  76. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/registry.rs +0 -0
  77. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/relation.rs +0 -0
  78. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/repo_cache.rs +0 -0
  79. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/repository.rs +0 -0
  80. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/sha256.rs +0 -0
  81. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/source_artifact.rs +0 -0
  82. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/spec.rs +0 -0
  83. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/staging.rs +0 -0
  84. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/storage.rs +0 -0
  85. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/store.rs +0 -0
  86. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/api.rs +0 -0
  87. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/hub.rs +0 -0
  88. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/mod.rs +0 -0
  89. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/policy.rs +0 -0
  90. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/prefix.rs +0 -0
  91. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/push.rs +0 -0
  92. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/transport/ranged.rs +0 -0
  93. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/src/vectors.rs +0 -0
  94. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/checkpoint_roots.rs +0 -0
  95. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fetch_url_progress.rs +0 -0
  96. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fit.rs +0 -0
  97. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fixtures/repo_apply/blobs.jsonl +0 -0
  98. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_list.jsonl +0 -0
  99. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_repository.json +0 -0
  100. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fixtures/repo_apply/expected_rows.jsonl +0 -0
  101. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fixtures/repo_apply/header.cbor +0 -0
  102. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fixtures/repo_apply/manifest.json +0 -0
  103. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/fixtures/repo_apply/mutation.json +0 -0
  104. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/h3_lora.rs +0 -0
  105. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/h3_swiglu.rs +0 -0
  106. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/home.rs +0 -0
  107. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/import_tree.rs +0 -0
  108. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/instrument_profiles.rs +0 -0
  109. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/liveness.rs +0 -0
  110. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/read_plan.rs +0 -0
  111. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/reclaim.rs +0 -0
  112. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/reclaim_run.rs +0 -0
  113. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/repo_cache.rs +0 -0
  114. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/repo_store.rs +0 -0
  115. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/resident_admission.rs +0 -0
  116. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/sha256_backends.rs +0 -0
  117. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/sharded_cas.rs +0 -0
  118. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/source_artifacts.rs +0 -0
  119. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/source_plan.rs +0 -0
  120. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/source_prefixes.rs +0 -0
  121. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/source_preflight.rs +0 -0
  122. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/staging.rs +0 -0
  123. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/store_boundaries.rs +0 -0
  124. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/tensor_schema.rs +0 -0
  125. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/version.rs +0 -0
  126. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-core/tests/write_through.rs +0 -0
  127. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-py/Cargo.toml +0 -0
  128. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-py/src/docs.rs +0 -0
  129. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-py/src/errors.rs +0 -0
  130. {tensorfs-0.3.45 → tensorfs-0.3.47}/crates/tensorfs-py/src/lib.rs +0 -0
  131. {tensorfs-0.3.45 → tensorfs-0.3.47}/python/tensorfs/__init__.py +0 -0
  132. {tensorfs-0.3.45 → tensorfs-0.3.47}/python/tensorfs/_derived_channel.py +0 -0
  133. {tensorfs-0.3.45 → tensorfs-0.3.47}/python/tensorfs/_source_channel.py +0 -0
  134. {tensorfs-0.3.45 → tensorfs-0.3.47}/python/tensorfs/_tensors.py +0 -0
  135. {tensorfs-0.3.45 → tensorfs-0.3.47}/python/tensorfs/derived.py +0 -0
  136. {tensorfs-0.3.45 → tensorfs-0.3.47}/python/tensorfs/errors.py +0 -0
  137. {tensorfs-0.3.45 → tensorfs-0.3.47}/python/tensorfs/py.typed +0 -0
@@ -421,7 +421,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
421
421
 
422
422
  [[package]]
423
423
  name = "tensorfs-core"
424
- version = "0.3.45"
424
+ version = "0.3.47"
425
425
  dependencies = [
426
426
  "fs2",
427
427
  "libc",
@@ -435,7 +435,7 @@ dependencies = [
435
435
 
436
436
  [[package]]
437
437
  name = "tensorfs-py"
438
- version = "0.3.45"
438
+ version = "0.3.47"
439
439
  dependencies = [
440
440
  "pyo3",
441
441
  "tensorfs-core",
@@ -3,5 +3,5 @@ resolver = "2"
3
3
  members = ["crates/tensorfs-core", "crates/tensorfs-py"]
4
4
 
5
5
  [workspace.package]
6
- version = "0.3.45"
6
+ version = "0.3.47"
7
7
  edition = "2021"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensorfs
3
- Version: 0.3.45
3
+ Version: 0.3.47
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Rust
@@ -11,7 +11,7 @@ path = "src/bin/tfs/main.rs"
11
11
  fs2 = "0.4"
12
12
  minicbor = { version = "2.3.0", default-features = false, features = ["alloc"] }
13
13
  rusqlite = { version = "0.32", features = ["bundled"] }
14
- rustix = { version = "1.1.4", default-features = false, features = ["fs", "std"] }
14
+ rustix = { version = "1.1.4", default-features = false, features = ["fs", "std", "net", "event"] }
15
15
  # tfs-049: the ONE transport (src/transport/). rustls is the one TLS stack, used only
16
16
  # there; scripts/fence.py refuses it anywhere else so a second mover cannot grow.
17
17
  rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
@@ -23,6 +23,9 @@ pub enum Edit {
23
23
  source_path: String,
24
24
  media_type: String,
25
25
  },
26
+ /// Inherit one already-committed asset from the source checkpoint header.
27
+ /// The asset declaration and segment ObjectRefs are copied verbatim; no payload is read.
28
+ Inherit { name: String, source_name: String },
26
29
  /// Remove an asset by its exact header name. Missing names are harmless, making
27
30
  /// retries deterministic.
28
31
  Drop { name: String },
@@ -72,6 +75,7 @@ pub fn update(
72
75
  detail: "asset update source has no CozyTensors header".into(),
73
76
  })?;
74
77
  let mut header = checkpoint::load_header(store, header_ref)?;
78
+ let source_assets: BTreeMap<String, Asset> = header.assets.iter().cloned().collect();
75
79
  let closure = checkpoint::load_closure(store, &header)?;
76
80
 
77
81
  let mut assets: BTreeMap<String, Asset> = header.assets.drain(..).collect();
@@ -80,6 +84,13 @@ pub fn update(
80
84
  Edit::Drop { name } => {
81
85
  assets.remove(name);
82
86
  }
87
+ Edit::Inherit { name, source_name } => {
88
+ let asset = source_assets.get(source_name).ok_or_else(|| Refusal {
89
+ code: Code::OBJECT_ABSENT,
90
+ detail: format!("source asset {source_name:?} is absent"),
91
+ })?;
92
+ assets.insert(name.clone(), asset.clone());
93
+ }
83
94
  Edit::Copy {
84
95
  name,
85
96
  source_path,
@@ -129,6 +140,47 @@ pub fn update(
129
140
  })
130
141
  }
131
142
 
143
+ /// Inherit selected runtime assets from one exact source checkpoint into another
144
+ /// metadata-only derived checkpoint. Tensor and asset payloads are never opened.
145
+ pub fn inherit(
146
+ store: &Store,
147
+ target_manifest: &ObjectRef,
148
+ source_manifest: &ObjectRef,
149
+ names: &[String],
150
+ ) -> Result<ResultSnapshot> {
151
+ let target = checkpoint::load_manifest(store, target_manifest)?;
152
+ let target_header_ref = target.header().ok_or_else(|| Refusal {
153
+ code: Code::ATTACHMENT_CARDINALITY,
154
+ detail: "asset inheritance target has no CozyTensors header".into(),
155
+ })?;
156
+ let source = checkpoint::load_manifest(store, source_manifest)?;
157
+ let source_header_ref = source.header().ok_or_else(|| Refusal {
158
+ code: Code::ATTACHMENT_CARDINALITY,
159
+ detail: "asset inheritance source has no CozyTensors header".into(),
160
+ })?;
161
+ let mut header = checkpoint::load_header(store, target_header_ref)?;
162
+ let source_header = checkpoint::load_header(store, source_header_ref)?;
163
+ let target_closure = checkpoint::load_closure(store, &header)?;
164
+ let source_assets: BTreeMap<String, Asset> = source_header.assets.into_iter().collect();
165
+ let mut assets: BTreeMap<String, Asset> = header.assets.drain(..).collect();
166
+ for name in names {
167
+ let asset = source_assets.get(name).ok_or_else(|| Refusal {
168
+ code: Code::OBJECT_ABSENT,
169
+ detail: format!("source asset {name:?} is absent"),
170
+ })?;
171
+ assets.insert(name.clone(), asset.clone());
172
+ }
173
+ header.assets = assets.into_iter().collect();
174
+ header.validate(&target_closure)?;
175
+ let header_object = checkpoint::put_doc(store, &header)?;
176
+ let manifest = checkpoint::build_manifest(&header, &header_object, &target_closure)?;
177
+ let manifest_object = store.put_manifest(&manifest)?.obj;
178
+ Ok(ResultSnapshot {
179
+ header: header_object,
180
+ manifest: manifest_object,
181
+ })
182
+ }
183
+
132
184
  #[cfg(test)]
133
185
  mod tests {
134
186
  use super::*;
@@ -290,4 +342,63 @@ mod tests {
290
342
  assert_eq!(one.manifest, two.manifest);
291
343
  std::fs::remove_dir_all(root).unwrap();
292
344
  }
345
+
346
+ #[test]
347
+ fn inherits_existing_asset_refs_without_payload_reads() {
348
+ let root = temp();
349
+ let store = Store::init(&root).unwrap();
350
+ let spec = registry::seeds().into_iter().next().unwrap().spec;
351
+ let payload = put(&store, b"tokenizer");
352
+ let header = Header {
353
+ configs: vec![],
354
+ assets: vec![(
355
+ "tokenizer/vocab.json".into(),
356
+ Asset {
357
+ logical_sha256: payload.sha256.clone(),
358
+ logical_length: payload.length,
359
+ media_type: "application/json".into(),
360
+ segments: vec![payload.clone()],
361
+ },
362
+ )],
363
+ encodings: vec![spec.clone()],
364
+ components: vec![(
365
+ "model".into(),
366
+ vec![(
367
+ "w".into(),
368
+ Tensor {
369
+ dtype: Dtype::U8,
370
+ shape: vec![1],
371
+ encoding: spec.object_id(),
372
+ parts: vec![(
373
+ "value".into(),
374
+ Part {
375
+ dtype: Dtype::U8,
376
+ shape: vec![1],
377
+ body: Body::Inline(vec![0]),
378
+ },
379
+ )],
380
+ },
381
+ )],
382
+ )],
383
+ };
384
+ let href = put(&store, &header.canonical_bytes().unwrap());
385
+ let source = Draft {
386
+ entries: vec![("model.cozytensors".into(), Entry::CozyTensors(href))],
387
+ }
388
+ .seal()
389
+ .unwrap();
390
+ let source_ref = store.put_manifest(&source).unwrap().obj;
391
+ let result = update(
392
+ &store,
393
+ &source_ref,
394
+ &[Edit::Inherit {
395
+ name: "tokenizer/vocab.json".into(),
396
+ source_name: "tokenizer/vocab.json".into(),
397
+ }],
398
+ )
399
+ .unwrap();
400
+ let output = checkpoint::load_header(&store, &result.header).unwrap();
401
+ assert_eq!(output.assets[0].1.segments, vec![payload]);
402
+ std::fs::remove_dir_all(root).unwrap();
403
+ }
293
404
  }
@@ -82,6 +82,7 @@ fn transfer_verdict(
82
82
  since: Instant,
83
83
  waiting_for: &str,
84
84
  ) -> Result<()> {
85
+ ledger.check_running()?;
85
86
  if judge != Judge::Transfer {
86
87
  return Ok(());
87
88
  }
@@ -131,6 +132,7 @@ fn transfer_verdict(
131
132
  /// its own window and advances only when transport bytes actually move; slow
132
133
  /// progress may span arbitrarily many windows without exhausting a wall budget.
133
134
  fn stream_verdict(judge: Judge, ledger: &Ledger, advanced: Instant, phase: &str) -> Result<()> {
135
+ ledger.check_running()?;
134
136
  if judge != Judge::Stream || advanced.elapsed() <= ledger.patience() {
135
137
  return Ok(());
136
138
  }
@@ -271,27 +273,86 @@ impl Wire {
271
273
  }
272
274
  }
273
275
 
274
- fn connect(checked: &CheckedUrl, deadline: Deadline, fail: Code) -> Result<TcpStream> {
276
+ fn connect(
277
+ checked: &CheckedUrl,
278
+ deadline: Deadline,
279
+ ledger: &Ledger,
280
+ judge: Judge,
281
+ fail: Code,
282
+ ) -> Result<TcpStream> {
283
+ use rustix::event::{poll, PollFd, PollFlags, Timespec};
284
+ use rustix::net::{self, AddressFamily, SocketFlags, SocketType};
275
285
  let mut last: Option<std::io::Error> = None;
276
- for addr in &checked.addrs {
277
- let attempt = match deadline.remaining()? {
278
- Some(left) => TcpStream::connect_timeout(addr, left),
279
- None => TcpStream::connect(addr),
286
+ 'addresses: for addr in &checked.addrs {
287
+ ledger.check_running()?;
288
+ let family = if addr.is_ipv4() {
289
+ AddressFamily::INET
290
+ } else {
291
+ AddressFamily::INET6
280
292
  };
281
- match attempt {
282
- Ok(sock) => {
283
- let _ = sock.set_nodelay(true);
284
- return Ok(sock);
293
+ let socket = match net::socket_with(
294
+ family,
295
+ SocketType::STREAM,
296
+ SocketFlags::NONBLOCK | SocketFlags::CLOEXEC,
297
+ Some(net::ipproto::TCP),
298
+ ) {
299
+ Ok(socket) => socket,
300
+ Err(error) => {
301
+ last = Some(error.into());
302
+ continue;
303
+ }
304
+ };
305
+ let since = Instant::now();
306
+ match net::connect(&socket, addr) {
307
+ Ok(()) => {}
308
+ Err(rustix::io::Errno::INPROGRESS) => loop {
309
+ // Poll one pending connection instead of restarting it at each
310
+ // sample. Cancellation and existing silence rules remain active
311
+ // without imposing a new total connection deadline.
312
+ transfer_verdict(judge, ledger, since, "a TCP connection")?;
313
+ stream_verdict(judge, ledger, since, "TCP connect")?;
314
+ let tick = deadline
315
+ .remaining()?
316
+ .map_or(ledger.sample(), |left| left.min(ledger.sample()));
317
+ let timeout = Timespec {
318
+ tv_sec: tick.as_secs() as _,
319
+ tv_nsec: tick.subsec_nanos() as _,
320
+ };
321
+ let mut descriptors = [PollFd::new(&socket, PollFlags::OUT)];
322
+ match poll(&mut descriptors, Some(&timeout)) {
323
+ Ok(0) | Err(rustix::io::Errno::INTR) => continue,
324
+ Ok(_) => match net::sockopt::socket_error(&socket) {
325
+ Ok(Ok(())) => break,
326
+ Ok(Err(error)) | Err(error) => {
327
+ last = Some(error.into());
328
+ continue 'addresses;
329
+ }
330
+ },
331
+ Err(error) => {
332
+ last = Some(error.into());
333
+ continue 'addresses;
334
+ }
335
+ }
336
+ },
337
+ Err(error) => {
338
+ last = Some(error.into());
339
+ continue;
285
340
  }
286
- Err(error) => last = Some(error),
287
341
  }
342
+ let socket: TcpStream = socket.into();
343
+ socket.set_nonblocking(false).map_err(|error| Refusal {
344
+ code: fail,
345
+ detail: format!("configure connected socket: {error}"),
346
+ })?;
347
+ let _ = socket.set_nodelay(true);
348
+ return Ok(socket);
288
349
  }
289
350
  refuse(
290
351
  fail,
291
352
  format!(
292
353
  "{}: no checked address accepted a connection ({})",
293
354
  checked.host,
294
- last.map(|e| e.to_string()).unwrap_or_default()
355
+ last.map(|error| error.to_string()).unwrap_or_default()
295
356
  ),
296
357
  )
297
358
  }
@@ -425,7 +486,11 @@ impl BodyReader<'_> {
425
486
  /// One raw read off the wire under the sampling rule: the socket timeout is the
426
487
  /// RESOLUTION; the deadline and (when judged) the measured patience carry the verdict.
427
488
  fn step(&mut self, buf: &mut [u8]) -> Result<usize> {
489
+ // Time spent by the consumer hashing or writing the prior chunk is not
490
+ // network silence. This wait starts when it asks for another chunk.
491
+ self.advanced = Instant::now();
428
492
  loop {
493
+ self.ledger.check_running()?;
429
494
  let tick = match self.deadline.remaining()? {
430
495
  Some(left) => left.min(self.ledger.sample()),
431
496
  None => self.ledger.sample(),
@@ -435,7 +500,9 @@ impl BodyReader<'_> {
435
500
  Ok(n) => {
436
501
  if n > 0 {
437
502
  let now = Instant::now();
438
- self.ledger.taught(now - self.advanced);
503
+ if self.judge == Judge::Stream {
504
+ self.ledger.taught(now - self.advanced);
505
+ }
439
506
  self.advanced = now;
440
507
  }
441
508
  return Ok(n);
@@ -645,8 +712,9 @@ pub fn request<'l>(
645
712
  write_silence: Option<Duration>,
646
713
  fail: Code,
647
714
  ) -> Result<Response<'l>> {
715
+ ledger.check_running()?;
648
716
  let opened = Instant::now();
649
- let sock = connect(checked, deadline, fail)?;
717
+ let sock = connect(checked, deadline, ledger, judge, fail)?;
650
718
  let mut wire = if checked.https {
651
719
  let name = ServerName::try_from(checked.host.clone()).map_err(|_| Refusal {
652
720
  code: Code::SOURCE_NOT_ALLOWED,
@@ -697,7 +765,9 @@ pub fn request<'l>(
697
765
 
698
766
  let (status, response_headers, spill) =
699
767
  read_head(&mut wire, deadline, ledger, judge, opened, fail)?;
700
- ledger.taught(opened.elapsed());
768
+ if judge == Judge::Stream {
769
+ ledger.taught(opened.elapsed());
770
+ }
701
771
  // A HEAD THAT ARRIVED IS PROGRESS. Without this, the second page of a paginated closure
702
772
  // would be judged from the start of the first, and a pull whose hub is merely slow
703
773
  // would condemn its own second call for the first call's honest latency.
@@ -31,6 +31,7 @@
31
31
  //! paid H100s sat at exactly zero for fifteen minutes with neither plane complaining.
32
32
 
33
33
  use crate::err::{refuse, Code, Result};
34
+ use std::sync::atomic::{AtomicBool, Ordering};
34
35
  use std::sync::Mutex;
35
36
  use std::time::{Duration, Instant};
36
37
 
@@ -134,6 +135,7 @@ pub struct Still {
134
135
 
135
136
  /// The one liveness ledger a pull shares across its streams.
136
137
  pub struct Ledger {
138
+ cancelled: AtomicBool,
137
139
  sample: Duration,
138
140
  worst_gap: Mutex<Duration>,
139
141
  progress: Mutex<Progress>,
@@ -150,6 +152,7 @@ impl Ledger {
150
152
  /// in milliseconds and judge by exactly the production rule.
151
153
  pub fn with_resolution(sample: Duration) -> Ledger {
152
154
  Ledger {
155
+ cancelled: AtomicBool::new(false),
153
156
  sample: sample.max(Duration::from_millis(1)),
154
157
  worst_gap: Mutex::new(Duration::ZERO),
155
158
  progress: Mutex::new(Progress {
@@ -181,6 +184,23 @@ impl Ledger {
181
184
  Instant::now()
182
185
  }
183
186
 
187
+ /// A terminal object failure stops the whole pull, including already-open
188
+ /// HTTP streams. Completed objects remain admitted for the next pull.
189
+ pub(crate) fn cancel(&self) {
190
+ self.cancelled.store(true, Ordering::Release);
191
+ }
192
+
193
+ pub(crate) fn check_running(&self) -> Result<()> {
194
+ if self.cancelled.load(Ordering::Acquire) {
195
+ refuse(
196
+ Code::TRANSFER_FAILED,
197
+ "the pull stopped after another object failed",
198
+ )
199
+ } else {
200
+ Ok(())
201
+ }
202
+ }
203
+
184
204
  pub fn sample(&self) -> Duration {
185
205
  self.sample
186
206
  }
@@ -207,6 +207,7 @@ pub fn fetch_authorized(
207
207
  // declared life, which is the only place that question can be answered.
208
208
  let mut stale: Option<u16> = None;
209
209
  for attempt in 1..=FETCH_ATTEMPTS {
210
+ ledger.check_running()?;
210
211
  let last = attempt == FETCH_ATTEMPTS;
211
212
  // A refused re-authorization must still name the origin's answer. It is the whole
212
213
  // diagnosis: "403, and re-minting was not honest" and "403, and the hub would not
@@ -604,6 +605,7 @@ pub(super) struct Leases<'a> {
604
605
 
605
606
  impl ObjectUrls for Leases<'_> {
606
607
  fn url_for(&self, at: usize, object: &ObjectRef, stale: bool) -> Result<String> {
608
+ self.ledger.check_running()?;
607
609
  if stale && !self.retire(&object.sha256)? {
608
610
  // The refused lease was NOT spent: the hub's own declared life still had more
609
611
  // than half of it left, so the origin's refusal is not about time and asking to
@@ -623,6 +625,7 @@ impl ObjectUrls for Leases<'_> {
623
625
  return Ok(url);
624
626
  }
625
627
  let _minting = self.minting.lock().unwrap();
628
+ self.ledger.check_running()?;
626
629
  // Another stream may have minted this window while this one waited for the right
627
630
  // to call. Asking again before calling is what makes one window cost one call.
628
631
  if let Some(url) = self.fresh(&object.sha256) {
@@ -773,7 +776,7 @@ pub struct WalkOutcome {
773
776
 
774
777
  /// Walk `plan.wanted` with `streams` objects in flight. One is the serial walk and it
775
778
  /// stays reachable, because a link where concurrency does not help is a real link. The
776
- /// first refusal wins; streams already moving finish their object and stop.
779
+ /// first refusal wins and cancels in-flight network waits. Verified objects remain local.
777
780
  ///
778
781
  /// **`cache` is the source order, and it is the whole reason a second pod is cheap.** The
779
782
  /// order is local Store, then the optional mounted cache, then the origin — the Store half
@@ -885,11 +888,14 @@ pub fn fetch_wanted(
885
888
  }
886
889
  }
887
890
  Err(refusal) => {
888
- stop.store(true, Ordering::Relaxed);
889
891
  let mut slot = first.lock().unwrap();
890
892
  if slot.is_none() {
893
+ // Keep the actual failure before waking peers, so their
894
+ // cancellation errors cannot replace its diagnosis.
891
895
  *slot = Some(refusal);
896
+ ledger.cancel();
892
897
  }
898
+ stop.store(true, Ordering::Relaxed);
893
899
  return;
894
900
  }
895
901
  }