@r1z4x/tezgah 1.3.0 → 1.5.0

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 (53) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/MANIFEST +3 -0
  3. package/agents/tezgah-reviewer.md +19 -10
  4. package/bin/tezgah-docs +3 -3
  5. package/bin/tezgah-doctor +75 -18
  6. package/bin/tezgah-route +5 -2
  7. package/bin/tezgah-setup +30 -18
  8. package/bin/tezgah-status +3 -1
  9. package/bin/tezgah-task +130 -37
  10. package/bin/tezgah-taste +74 -55
  11. package/bin/tezgah-triage +12 -7
  12. package/docs/architecture.md +4 -4
  13. package/docs/clarity-baseline.json +9 -9
  14. package/docs/contract.md +22 -12
  15. package/docs/environment.md +4 -1
  16. package/docs/evidence.md +71 -34
  17. package/docs/gate.md +21 -21
  18. package/docs/glossary.md +5 -5
  19. package/docs/hosts.md +8 -8
  20. package/docs/judge.md +93 -27
  21. package/docs/operations.md +69 -30
  22. package/docs/skills.md +2 -2
  23. package/docs/status-line.md +7 -7
  24. package/docs/vision.md +10 -5
  25. package/hooks/tezgah_agents.py +22 -11
  26. package/hooks/tezgah_attest.py +15 -22
  27. package/hooks/tezgah_context.py +84 -124
  28. package/hooks/tezgah_gate.py +26 -23
  29. package/hooks/tezgah_guard.py +9 -11
  30. package/hooks/tezgah_integrity.py +77 -178
  31. package/hooks/tezgah_judge.py +270 -122
  32. package/hooks/tezgah_lessons.py +18 -63
  33. package/hooks/tezgah_paths.py +183 -55
  34. package/hooks/tezgah_policy.py +35 -8
  35. package/hooks/tezgah_replay.py +57 -33
  36. package/hooks/tezgah_shapes.py +4 -4
  37. package/hooks/tezgah_skill_pick.py +6 -35
  38. package/hooks/tezgah_snapshot.py +25 -46
  39. package/hooks/tezgah_store.py +1268 -0
  40. package/hooks/tezgah_task.py +146 -40
  41. package/hooks/tezgah_taste.py +21 -18
  42. package/hooks/tezgah_taste_ledger.py +63 -88
  43. package/hooks/tezgah_update.py +6 -26
  44. package/hosts/codex/hook.py +5 -0
  45. package/hosts/cursor/hook.py +19 -31
  46. package/hosts/opencode/plugins/tezgah.js +309 -108
  47. package/package.json +1 -1
  48. package/skills/harness/SKILL.md +4 -0
  49. package/skills/plan-add/SKILL.md +23 -7
  50. package/skills/plan-add/render_table.py +3 -2
  51. package/skills/plan-status/SKILL.md +12 -4
  52. package/skills/plan-sync/SKILL.md +19 -9
  53. package/skills/tezgah-contract/SKILL.md +25 -12
@@ -6,7 +6,7 @@ code, a blind label sheet drawn from them, and the report that reads the labels
6
6
  tezgah-gate replay [--cutoff WHEN] [--since WHEN] [--json]
7
7
  tezgah-gate replay --report [--run DIR] [--labels FILE ...] [--json]
8
8
  tezgah-gate replay --sheet [--run DIR] [--seed N] [--rules FILE]
9
- tezgah-gate replay --label-model --prompt FILE [--provider typesafe|deepseek]
9
+ tezgah-gate replay --label-model --prompt FILE [--provider jev|deepseek]
10
10
  [--model ID] [--run DIR]
11
11
 
12
12
  **Corpus.** Every ledger under the cache's `evidence/` is read read-only; a
@@ -40,8 +40,10 @@ call's own output.
40
40
 
41
41
  **Output.** Everything is written under `~/.cache/tezgah/replay/<run>/`, owner
42
42
  only, and never leaves the machine: `corpus.jsonl` (the items, with their
43
- inputs), `results.jsonl`, `summary.json`, and the sheet files. The sandbox HOME
44
- and the row stream are deleted when the child returns.
43
+ inputs), `results.jsonl` and the sheet files. The run's summary is its row in
44
+ the cache database's replay index (`tezgah_store.add_replay`), whose newest
45
+ row is the latest run. The sandbox HOME and the row stream are deleted when
46
+ the child returns.
45
47
  """
46
48
  import argparse
47
49
  import datetime
@@ -64,6 +66,7 @@ if HERE not in sys.path:
64
66
  sys.path.insert(0, HERE)
65
67
  import tezgah_integrity as ti # noqa: E402
66
68
  import tezgah_paths as tp # noqa: E402
69
+ import tezgah_store # noqa: E402
67
70
 
68
71
  # The one rule-label derivation; nothing in this module maps text to a rule.
69
72
  rule_of = ti._deny_rule
@@ -425,9 +428,6 @@ def child(run):
425
428
  import tezgah_gate as tg
426
429
  items = {it["i"]: it for it in _read_jsonl(os.path.join(run, "corpus.jsonl"))}
427
430
  evidence = os.path.join(ti.cache_dir(), "evidence")
428
- nudged = os.path.join(ti.cache_dir(), "nudged")
429
- os.makedirs(evidence, mode=0o700, exist_ok=True)
430
- os.makedirs(nudged, mode=0o700, exist_ok=True)
431
431
  now = [0.0]
432
432
 
433
433
  def frozen():
@@ -454,15 +454,13 @@ def child(run):
454
454
  now[0] = float(it["ts"])
455
455
  results.append(_replay_one(it, rows_of[fname], tg, seen, live_events))
456
456
  rows_of[fname].append(row)
457
- path = os.path.join(evidence, fname)
458
- with open(path, "a", encoding="utf-8") as out:
459
- out.write(json.dumps(row) + "\n")
460
- if isinstance(row.get("ts"), (int, float)):
461
- os.utime(path, (row["ts"], row["ts"]))
457
+ # the row as the live writer stored it; its own `ts` is the time the
458
+ # cross-session read (`writers_elsewhere`) windows on
459
+ ti._append(os.path.join(evidence, fname), json.dumps(row) + "\n")
462
460
  if row.get("kind") == "nudge":
463
461
  # the once-per-session nudge mark the live gate wrote with this
464
- # row (`first_nudge`): disk state the ledger records
465
- open(tg.nudge_mark(sid), "w", encoding="utf-8").close()
462
+ # row (`first_nudge`): cache state the ledger records
463
+ tezgah_store.mark("nudged", tg.nudge_key(sid))
466
464
  _write_jsonl(os.path.join(run, "results.jsonl"), results)
467
465
 
468
466
 
@@ -597,8 +595,7 @@ def run_replay(cutoff=None, since=None):
597
595
  "fidelity": fidelity(items, results),
598
596
  "race_family": family,
599
597
  "deny_runs": folds}
600
- _owner_write(os.path.join(run, "summary.json"), json.dumps(summary, indent=2) + "\n")
601
- _owner_write(os.path.join(replay_root(), "latest"), run + "\n")
598
+ tezgah_store.add_replay(_run_key(run), summary, tp.CACHE)
602
599
  return summary
603
600
 
604
601
 
@@ -671,7 +668,7 @@ def sheet(run, seed=SEED, rules=None):
671
668
  """The blind sample: `sheet.jsonl` ({set, n, text}), `sheet-key.jsonl` (the
672
669
  hidden live verdicts) and `instructions.md`. Returns (paths, counts)."""
673
670
  items = _read_jsonl(os.path.join(run, "corpus.jsonl"))
674
- summary = json.load(open(os.path.join(run, "summary.json"), encoding="utf-8"))
671
+ summary = _summary(run)
675
672
  rng = random.Random(seed)
676
673
  pools = defaultdict(list)
677
674
  # A race deny written before the ledger stored `target` cannot show its
@@ -790,7 +787,7 @@ def _rate(k, n):
790
787
 
791
788
 
792
789
  def report(run, label_files=()):
793
- summary = json.load(open(os.path.join(run, "summary.json"), encoding="utf-8"))
790
+ summary = _summary(run)
794
791
  out = {k: summary[k] for k in ("run", "cutoff", "cutoff_local", "ledgers", "candidates",
795
792
  "excluded", "items", "joined", "join",
796
793
  "stop_text_fidelity", "fidelity", "race_family",
@@ -867,9 +864,9 @@ def report(run, label_files=()):
867
864
 
868
865
  # ----------------------------------------------------------------- raters --
869
866
 
870
- # Rater 2 (H2 amendment 2026-10-07): a model through the judge seam, TypeSafe
871
- # only, billed per million input tokens and nothing for output, at the price
872
- # `bin/tezgah-triage::PRICE_PER_MILLION` quotes.
867
+ # Rater 2 (H2 amendment 2026-10-07): the Jev model through the judge seam, any
868
+ # Jev carrier (`tezgah_judge.JEV_CARRIERS`), billed per million input tokens and
869
+ # nothing for output, at the price `bin/tezgah-triage::PRICE_PER_MILLION` quotes.
873
870
  RATER_MODEL = "jev-latest"
874
871
  JEV_PRICE_PER_MILLION = 0.042
875
872
  LABEL_WORKERS = 4
@@ -906,9 +903,10 @@ def _deepseek(secret, model):
906
903
  return ask
907
904
 
908
905
 
909
- def label_model(run, prompt_file, provider="typesafe", model=None):
906
+ def label_model(run, prompt_file, provider="jev", model=None):
910
907
  """One `choice` question per blind sheet row, the row's text redacted
911
- (`ti.redact`). `typesafe` asks through the judge seam and writes
908
+ (`ti.redact`). `jev` (old name `typesafe`) asks the Jev model through the
909
+ judge seam, whichever carrier resolves, and writes
912
910
  `labels-model.jsonl` (rater 2); `deepseek` asks DeepSeek's chat endpoint and
913
911
  writes `labels-deepseek.jsonl` (rater 1). Rows already labelled there are not
914
912
  asked again; a reply without a label among the prompt's criteria stays
@@ -923,13 +921,15 @@ def label_model(run, prompt_file, provider="typesafe", model=None):
923
921
  ask, rater = _deepseek(secret, model or "deepseek-v4-pro"), "deepseek"
924
922
  price = DEEPSEEK_PRICE_PER_MILLION
925
923
  else:
926
- used = tj.credential()[0]
927
- if used != "typesafe" or not tj.available():
928
- raise SystemExit("label-model: the judge seam must answer through TypeSafe "
929
- "(provider: %s)" % used)
924
+ if tj.tp.off("judge-off") or not tj.named(("jev",)):
925
+ raise SystemExit("label-model: the judge seam must answer through a Jev "
926
+ "carrier (TypeSafe, OpenRouter System One, Cloudflare "
927
+ "Workers AI or JEV_API_BASE_URL; TEZGAH_JEV_PROVIDER picks "
928
+ "one), and none resolves")
930
929
 
931
930
  def ask(state, questions):
932
- return tj.ask(state, questions, model=model or RATER_MODEL, timeout=60)
931
+ return tj.ask(state, questions, model=model or RATER_MODEL, timeout=60,
932
+ only=("jev",))
933
933
  rater, price = "model", (JEV_PRICE_PER_MILLION, 0.0)
934
934
  with open(prompt_file, encoding="utf-8") as fh:
935
935
  prompt = json.load(fh)
@@ -945,7 +945,7 @@ def label_model(run, prompt_file, provider="typesafe", model=None):
945
945
  label = tj.choice(result, "label")
946
946
  return row, result, label if label in question["criteria"] else None
947
947
 
948
- tokens, out_tokens, models, new = 0, 0, Counter(), 0
948
+ tokens, out_tokens, models, new, spent, prices = 0, 0, Counter(), 0, 0.0, set()
949
949
  fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600)
950
950
  os.fchmod(fd, 0o600)
951
951
  # Each answered row is appended as it arrives, so a run that dies mid-way
@@ -956,6 +956,12 @@ def label_model(run, prompt_file, provider="typesafe", model=None):
956
956
  tokens += result["usage"]["input_tokens"]
957
957
  out_tokens += result["usage"]["output_tokens"]
958
958
  models["%s/%s" % (result["provider"], result["model"])] += 1
959
+ # a typed carrier is billed at its own price (Clef's is not Jev's)
960
+ typed = tj.input_price(result["provider"])
961
+ row_price = (typed, 0.0) if typed is not None else price
962
+ prices.add(row_price)
963
+ spent += (result["usage"]["input_tokens"] * row_price[0]
964
+ + result["usage"]["output_tokens"] * row_price[1]) / 1e6
959
965
  if label:
960
966
  fh.write(json.dumps({"set": row["set"], "n": row["n"], "label": label,
961
967
  "rater": rater, "model": result["model"],
@@ -965,8 +971,9 @@ def label_model(run, prompt_file, provider="typesafe", model=None):
965
971
  new += 1
966
972
  return {"path": path, "asked": len(todo), "labelled": new,
967
973
  "total": len(done) + new, "input_tokens": tokens, "output_tokens": out_tokens,
968
- "price": price,
969
- "cost_usd": (tokens * price[0] + out_tokens * price[1]) / 1e6,
974
+ # the printed formula names the rate the rows were billed at, when one
975
+ "price": prices.pop() if len(prices) == 1 else price,
976
+ "cost_usd": spent,
970
977
  "models": dict(models)}
971
978
 
972
979
 
@@ -1010,8 +1017,24 @@ def _pct(k, n):
1010
1017
  def _run_dir(arg):
1011
1018
  if arg:
1012
1019
  return arg
1013
- with open(os.path.join(replay_root(), "latest"), encoding="utf-8") as fh:
1014
- return fh.read().strip()
1020
+ run = tezgah_store.replay_latest(tp.CACHE)
1021
+ if not run:
1022
+ raise SystemExit("replay: no run yet; run `tezgah-gate replay` first")
1023
+ return run
1024
+
1025
+
1026
+ def _run_key(run):
1027
+ """A run's key in the replay index: its directory's real path, so a
1028
+ relative or linked `--run` names the run its row was written under."""
1029
+ return os.path.realpath(run)
1030
+
1031
+
1032
+ def _summary(run):
1033
+ """The summary the run in directory `run` was indexed with (`run_replay`)."""
1034
+ summary = tezgah_store.replay_summary(_run_key(run), tp.CACHE)
1035
+ if summary is None:
1036
+ raise SystemExit("replay: no indexed run at %s" % run)
1037
+ return summary
1015
1038
 
1016
1039
 
1017
1040
  def main(argv):
@@ -1021,7 +1044,8 @@ def main(argv):
1021
1044
  p.add_argument("--report", action="store_true")
1022
1045
  p.add_argument("--label-model", action="store_true")
1023
1046
  p.add_argument("--prompt")
1024
- p.add_argument("--provider", choices=("typesafe", "deepseek"), default="typesafe")
1047
+ # `typesafe` is the old name of `jev`, kept for the commands already written down
1048
+ p.add_argument("--provider", choices=("jev", "typesafe", "deepseek"), default="jev")
1025
1049
  p.add_argument("--model")
1026
1050
  p.add_argument("--sheet", action="store_true")
1027
1051
  p.add_argument("--run")
@@ -212,7 +212,7 @@ def rule_yield(files=None, shipped=None):
212
212
  `rule_ship_times()`. Fixture ledgers are left out by `ti.fixture_ledger`, the
213
213
  exclusion `counters_all` applies, because a probe run measures the harness
214
214
  and not use. A rule's exposure is the real ledgers last written at or after
215
- it shipped (the file's mtime), and its fires are the deny rows under its
215
+ it shipped (the newest row's `ts`), and its fires are the deny rows under its
216
216
  label in those ledgers. `mark` is `retire-candidate` at or below
217
217
  `RETIRE_FIRES` fires over at least `MIN_EXPOSURE` ledgers, `low-exposure` at
218
218
  or below `RETIRE_FIRES` over fewer (or an unknown ship date), else None.
@@ -227,9 +227,9 @@ def rule_yield(files=None, shipped=None):
227
227
  rows = ti._foreign_rows(path)
228
228
  if ti.fixture_ledger(rows):
229
229
  continue
230
- try:
231
- when = os.path.getmtime(path)
232
- except OSError:
230
+ when = max((r["ts"] for r in rows if isinstance(r.get("ts"), (int, float))),
231
+ default=None)
232
+ if when is None:
233
233
  continue
234
234
  real.append((when, Counter(_label(row.get("detail")) for row in rows
235
235
  if row.get("kind") == "deny")))
@@ -58,6 +58,7 @@ import tezgah_integrity as ti
58
58
  import tezgah_judge
59
59
  import tezgah_rank
60
60
  import tezgah_paths as tp
61
+ import tezgah_store
61
62
 
62
63
  ARM = "skill-suggest-on"
63
64
  # Below this, the turn's own gate question says no skill is wanted and nothing is
@@ -216,17 +217,8 @@ def judge(prompt, names, session_id=""):
216
217
  roots=[os.path.join(tp.PLUGIN_ROOT, "skills")]))
217
218
 
218
219
 
219
- def _path(session_id):
220
- return os.path.join(tp.cache_dir(), "skill-pick",
221
- slug(session_id) + ".json")
222
-
223
-
224
220
  def _remembered(session_id):
225
- try:
226
- with open(_path(session_id), encoding="utf-8") as fh:
227
- data = json.load(fh)
228
- except (OSError, ValueError):
229
- return {}
221
+ data = tezgah_store.doc("skill_pick", slug(session_id))
230
222
  return data if isinstance(data, dict) else {}
231
223
 
232
224
 
@@ -237,13 +229,7 @@ def _remember(session_id, digest, line):
237
229
  data[digest] = line
238
230
  for old in list(data)[:max(0, len(data) - KEPT)]:
239
231
  del data[old]
240
- path = _path(session_id)
241
- try:
242
- os.makedirs(os.path.dirname(path), exist_ok=True)
243
- with open(path, "w", encoding="utf-8") as fh:
244
- json.dump(data, fh)
245
- except OSError:
246
- pass
232
+ tezgah_store.put_doc("skill_pick", slug(session_id), data)
247
233
 
248
234
 
249
235
  def suggest(prompt, session_id=""):
@@ -617,31 +603,16 @@ def top_section(prompt, roots=None):
617
603
  return tuple(hit), covered, len(known)
618
604
 
619
605
 
620
- def _hint_path(session_id):
621
- return os.path.join(tp.cache_dir(), "skill-section-hint",
622
- slug(session_id) + ".json")
623
-
624
-
625
606
  def _shown(session_id):
626
607
  """The sections this session was already pointed at, ["path:start"]."""
627
- try:
628
- with open(_hint_path(session_id), encoding="utf-8") as fh:
629
- data = json.load(fh)
630
- except (OSError, ValueError):
631
- return []
608
+ data = tezgah_store.doc("section_hint", slug(session_id))
632
609
  return data if isinstance(data, list) else []
633
610
 
634
611
 
635
612
  def _remember_shown(session_id, shown):
636
- """Bounded and best effort, like the judgement's cache: a dir that cannot
613
+ """Bounded and best effort, like the judgement's cache: a store that cannot
637
614
  be written costs a repeated line, never the turn."""
638
- path = _hint_path(session_id)
639
- try:
640
- os.makedirs(os.path.dirname(path), exist_ok=True)
641
- with open(path, "w", encoding="utf-8") as fh:
642
- json.dump(shown[-KEPT:], fh)
643
- except OSError:
644
- pass
615
+ tezgah_store.put_doc("section_hint", slug(session_id), shown[-KEPT:])
645
616
 
646
617
 
647
618
  def section_hint(prompt, session_id="", host=None, roots=None):
@@ -24,11 +24,12 @@ import time
24
24
  import uuid
25
25
 
26
26
  import tezgah_integrity as ti
27
+ import tezgah_store
27
28
  from tezgah_paths import cache_dir, root_for
28
29
 
29
30
  # A snapshot id is exactly what `capture` generates: 12 hex chars. Validated
30
31
  # before it is used as a path segment, so an id typed at a shell cannot walk out
31
- # of the store with `..` or an absolute path and read some other file's manifest.
32
+ # of the store with `..` or an absolute path and read some other file as a blob.
32
33
  SNAPSHOT_ID = re.compile(r"\A[0-9a-f]{12}\Z")
33
34
 
34
35
  # The store is capped, because copying every file the agent touches with no
@@ -41,7 +42,9 @@ SNAPSHOT_ID = re.compile(r"\A[0-9a-f]{12}\Z")
41
42
  # mistake is most likely and most expensive, while the snapshot anyone actually
42
43
  # asks for is about a recent mistake - the oldest one is what nobody is coming
43
44
  # back for. Worst case on disk is CAP * MAX_BYTES = 400 MB; the realistic case
44
- # is a few MB, because source files are kilobytes.
45
+ # is a few MB, because source files are kilobytes. Each blob is a file in its
46
+ # own directory; the manifests are the `snapshots` rows of the cache database,
47
+ # in capture order, and that order is what eviction reads.
45
48
  #
46
49
  # MAX_BYTES per file, above which nothing is captured at all. `capture` runs
47
50
  # synchronously in the gate a host calls before the write, so copying a
@@ -61,10 +64,10 @@ HASH_MAX_BYTES = 8 * MAX_BYTES
61
64
 
62
65
 
63
66
  def _store():
64
- """Where the pre-write bytes go: beside the evidence ledger, in whichever
65
- cache dir tezgah settled on for this environment (a sandboxed host has no
66
- writable ~/.cache and falls back to temp), so all tezgah state sits under
67
- one root."""
67
+ """Where the pre-write bytes go: beside the cache database that indexes
68
+ them, in whichever cache dir tezgah settled on for this environment (a
69
+ sandboxed host has no writable ~/.cache and falls back to temp), so all
70
+ tezgah state sits under one root."""
68
71
  return os.path.join(cache_dir(), "snapshots")
69
72
 
70
73
 
@@ -76,34 +79,13 @@ def _blob(snapshot_id):
76
79
  return os.path.join(_dir(snapshot_id), "file")
77
80
 
78
81
 
79
- def _manifest(snapshot_id):
80
- return os.path.join(_dir(snapshot_id), "meta.json")
81
-
82
-
83
- def _dirs():
84
- try:
85
- return [os.path.join(_store(), n) for n in os.listdir(_store())]
86
- except OSError:
87
- return []
88
-
89
-
90
- def _evict(keep=None):
91
- """Drop snapshot directories, oldest first, until `keep` (CAP) remain.
92
-
93
- The order is each directory's own mtime, which is when its capture created
94
- it - nothing writes inside a snapshot directory afterwards. Best effort: an
95
- entry that cannot be read or removed costs the cap, never the capture the
96
- gate is waiting on."""
97
- keep = CAP if keep is None else keep
98
- try:
99
- # ns rather than the float mtime: two captures inside one tick must not
100
- # evict an arbitrary one of the pair
101
- entries = sorted((os.stat(p).st_mtime_ns, p) for p in _dirs())
102
- except OSError:
103
- return
104
- for _, path in entries[:-keep]:
105
- import shutil # deferred: ~3.7 ms of import for a path only a write takes
106
- shutil.rmtree(path, ignore_errors=True)
82
+ def _drop(snapshot_ids):
83
+ """Remove the blob directories of `snapshot_ids`. Best effort: an entry
84
+ that cannot be removed costs disk, never the capture the gate is waiting
85
+ on."""
86
+ import shutil # deferred: ~3.7 ms of import for a path only a write takes
87
+ for sid in snapshot_ids:
88
+ shutil.rmtree(_dir(sid), ignore_errors=True)
107
89
 
108
90
 
109
91
  def _hash_file(path):
@@ -222,21 +204,19 @@ def _capture_one(path, cwd, session_id):
222
204
 
223
205
 
224
206
  def _save(data, meta):
225
- """Write one blob and its manifest under a fresh id, evict past CAP, and
226
- return the id - or None, with nothing left behind, when the write failed."""
207
+ """Write one blob, index it with its manifest, evict past CAP, and return
208
+ the id - or None, with nothing left behind, when the write failed."""
227
209
  sid = uuid.uuid4().hex[:12]
228
210
  try:
229
211
  os.makedirs(_store(), mode=0o700, exist_ok=True)
230
212
  os.makedirs(_dir(sid), mode=0o700, exist_ok=True)
231
213
  with _private_open(_blob(sid)) as fh:
232
214
  fh.write(data)
233
- with _private_open(_manifest(sid)) as fh:
234
- fh.write(json.dumps(dict(meta, id=sid)).encode("utf-8"))
235
- except OSError:
236
- import shutil # deferred: see _evict
237
- shutil.rmtree(_dir(sid), ignore_errors=True) # no half a snapshot
215
+ evicted = tezgah_store.add_snapshot(sid, dict(meta, id=sid), CAP)
216
+ except tezgah_store.ERRORS:
217
+ _drop([sid]) # no half a snapshot
238
218
  return None
239
- _evict()
219
+ _drop(evicted)
240
220
  return sid
241
221
 
242
222
 
@@ -321,14 +301,13 @@ def restore(snapshot_id, force=False, expect=None):
321
301
  sid = str(snapshot_id or "")
322
302
  meta, data = None, b""
323
303
  if SNAPSHOT_ID.match(sid):
304
+ meta = next(iter(tezgah_store.snapshots(sid)), None)
324
305
  try:
325
- with open(_manifest(sid), encoding="utf-8") as fh:
326
- meta = json.load(fh)
327
306
  with open(_blob(sid), "rb") as fh:
328
307
  data = fh.read()
329
- except (OSError, ValueError):
308
+ except OSError:
330
309
  meta = None
331
- if not meta:
310
+ if not isinstance(meta, dict):
332
311
  raise SnapshotError(
333
312
  "unknown snapshot id %r: the store holds no snapshot for it (CAP "
334
313
  "keeps the newest %d, and an evicted one is gone for good)"