cctally 1.117.2 → 1.118.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 (58) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -3
  3. package/bin/_cctally_cache.py +2783 -175
  4. package/bin/_cctally_completion.py +106 -33
  5. package/bin/_cctally_core.py +69 -4
  6. package/bin/_cctally_dashboard.py +188 -13
  7. package/bin/_cctally_dashboard_conversation.py +5 -1
  8. package/bin/_cctally_dashboard_envelope.py +50 -19
  9. package/bin/_cctally_dashboard_sources.py +23 -1
  10. package/bin/_cctally_db.py +193 -153
  11. package/bin/_cctally_diagnosis_sources.py +63 -7
  12. package/bin/_cctally_doctor.py +865 -93
  13. package/bin/_cctally_five_hour.py +67 -50
  14. package/bin/_cctally_forecast.py +35 -13
  15. package/bin/_cctally_journal.py +186 -18
  16. package/bin/_cctally_milestone_history.py +859 -62
  17. package/bin/_cctally_oauth_samples.py +286 -15
  18. package/bin/_cctally_parser.py +2 -2
  19. package/bin/_cctally_quota.py +104 -12
  20. package/bin/_cctally_record.py +153 -29
  21. package/bin/_cctally_rederive.py +608 -68
  22. package/bin/_cctally_statusline.py +108 -81
  23. package/bin/_cctally_store.py +179 -38
  24. package/bin/_cctally_tui.py +892 -181
  25. package/bin/_cctally_update.py +32 -13
  26. package/bin/_lib_codex_conversation_query.py +52 -14
  27. package/bin/_lib_codex_drain_admission.py +224 -6
  28. package/bin/_lib_codex_metadata.py +39 -0
  29. package/bin/_lib_conversation_anon.py +15 -8
  30. package/bin/_lib_conversation_dispatch.py +7 -0
  31. package/bin/_lib_conversation_query.py +289 -70
  32. package/bin/_lib_dashboard_sources.py +37 -1
  33. package/bin/_lib_detector_access_registry.py +497 -26
  34. package/bin/_lib_doctor.py +85 -20
  35. package/bin/_lib_doctor_scan_index.py +321 -0
  36. package/bin/_lib_ingest_frontier.py +20 -1
  37. package/bin/_lib_oauth_samples.py +23 -7
  38. package/bin/_lib_pricing.py +146 -16
  39. package/bin/_lib_quota.py +19 -0
  40. package/bin/_lib_rederive.py +11 -6
  41. package/bin/_lib_retained_records.py +516 -0
  42. package/bin/_lib_snapshot_cache.py +10 -0
  43. package/bin/_lib_stale_proof.py +272 -43
  44. package/bin/_lib_stats_content_revision.py +831 -0
  45. package/bin/_lib_write_io.py +11 -4
  46. package/bin/cctally +4 -1
  47. package/dashboard/static/assets/ConversationsView-D4Iv4ku0.js +72 -0
  48. package/dashboard/static/assets/{DoctorModal-B0DnU9d9.js → DoctorModal-D4QSq6uz.js} +1 -1
  49. package/dashboard/static/assets/ModalRoot-C9F5dhZi.js +1 -0
  50. package/dashboard/static/assets/{ProjectsDrillPanel-DSxXQSa-.js → ProjectsDrillPanel-WxAdRDMt.js} +1 -1
  51. package/dashboard/static/assets/{SourceDetailModal-BleDpQqG.js → SourceDetailModal-NyyEfXmc.js} +1 -1
  52. package/dashboard/static/assets/{UpdateModal-CMqaAkSK.js → UpdateModal-CFLq9pT1.js} +1 -1
  53. package/dashboard/static/assets/{index-BjQV-0-D.js → index-BKTWH9JE.js} +2 -2
  54. package/dashboard/static/assets/{outlineNavigation-CO-VWBm7.js → outlineNavigation-BnGUwCS3.js} +3 -3
  55. package/dashboard/static/dashboard.html +1 -1
  56. package/package.json +4 -1
  57. package/dashboard/static/assets/ConversationsView-oqNsWzWV.js +0 -72
  58. package/dashboard/static/assets/ModalRoot-CYua2wP_.js +0 -1
@@ -18,6 +18,8 @@ Spec: docs/superpowers/specs/2026-05-13-bin-cctally-split-design.md
18
18
  from __future__ import annotations
19
19
 
20
20
  import dataclasses
21
+ import hashlib
22
+ import json
21
23
  import datetime as dt
22
24
  import math
23
25
  import re
@@ -81,9 +83,11 @@ def _chip_for_model(name: str) -> str:
81
83
  return "other"
82
84
 
83
85
 
84
- # Date the embedded pricing snapshots below were last verified against
85
- # vendor sources. Bump whenever CLAUDE_MODEL_PRICING / CODEX_MODEL_PRICING
86
- # is synced. Read by `pricing-check` + the release pre-flight staleness nudge.
86
+ # Date of the latest pricing sync of the embedded tables below. Bump whenever
87
+ # CLAUDE_MODEL_PRICING / CODEX_MODEL_PRICING is synced. A targeted sync
88
+ # re-verifies only the cards it names (each table's header log says which);
89
+ # it does not re-verify every card. Read by `pricing-check` + the release
90
+ # pre-flight staleness nudge.
87
91
  #
88
92
  # CONTRACT (#705): a pricing revision must always ADVANCE this date. A second
89
93
  # revision within one UTC day therefore takes the NEXT day's date rather than
@@ -97,7 +101,7 @@ def _chip_for_model(name: str) -> str:
97
101
  # pinned in `bin/_lib_detector_access_registry.py`, so its registry test fails
98
102
  # until the affected pins are re-pinned through the reviewed procedure in that
99
103
  # module's docstring. Bulk regeneration of the pins alone is not acceptance.
100
- PRICING_SNAPSHOT_DATE = "2026-10-14"
104
+ PRICING_SNAPSHOT_DATE = "2026-11-05"
101
105
  PRICING_STALENESS_DAYS = 60 # release pre-flight WARNs past this age
102
106
 
103
107
 
@@ -324,6 +328,16 @@ LITELLM_PRICES_URL = (
324
328
  # `diff_pricing` keys on raw table membership and is deliberately NOT
325
329
  # alias-aware, so the omission needs this entry. It is self-policing: it goes
326
330
  # stale automatically if upstream drops `gpt-5.6` or we restore a direct card.
331
+ #
332
+ # gpt-5.5-cyber-preview (#989): also model-only. OpenAI publishes no row, model
333
+ # page or alias statement for this identifier, which third-party press ties
334
+ # to GPT-5.5-Cyber's May 2026 limited preview. LiteLLM prices it with cost
335
+ # fields identical to
336
+ # `gpt-5.5-cyber`, whose card OpenAI's pricing page does list, so
337
+ # CODEX_MODEL_ALIASES maps it there as an INFERRED pricing equivalence rather
338
+ # than a vendor-declared alias. This entry polices membership only: it goes
339
+ # stale if upstream drops the identifier or a direct card is added, but a
340
+ # preview-specific repricing upstream would NOT surface here.
327
341
  PRICING_DRIFT_ALLOWLIST: list[dict] = [
328
342
  {
329
343
  "model": "claude-mythos-preview",
@@ -374,6 +388,17 @@ PRICING_DRIFT_ALLOWLIST: list[dict] = [
374
388
  "(#643)."
375
389
  ),
376
390
  },
391
+ {
392
+ "model": "gpt-5.5-cyber-preview",
393
+ "reason": (
394
+ "OpenAI publishes no price or alias for gpt-5.5-cyber-preview. "
395
+ "LiteLLM's cost fields for it equal gpt-5.5-cyber's (observed "
396
+ "2026-11-05), whose card OpenAI lists, so CODEX_MODEL_ALIASES "
397
+ "maps it there as an inferred equivalence and this table carries "
398
+ "no duplicate card. This entry polices membership only; a "
399
+ "preview-specific repricing upstream would not surface (#989)."
400
+ ),
401
+ },
377
402
  ]
378
403
 
379
404
  # Anthropic API pricing snapshot:
@@ -748,13 +773,14 @@ _unknown_model_warnings: set[str] = set()
748
773
  # Codex (OpenAI) API pricing snapshot:
749
774
  # - Source: https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json
750
775
  # - Captured: 2026-07-19 — the last FULL Codex sync. PRICING_SNAPSHOT_DATE has
751
- # since moved for six targeted syncs (2026-07-24, the Claude-side opus-5
752
- # sync; 2026-07-31, the gpt-5.6-terra/-luna correction logged below;
753
- # 2026-08-13, the Claude-side Sonnet/Mythos sync above; 2026-08-25, the
754
- # gpt-5.6-cyber addition logged below; 2026-09-05, the gpt-6-astra
755
- # addition; and 2026-09-22, the gpt-6-sol addition logged below). Codex
756
- # values outside those four Codex corrections were NOT re-verified on those
757
- # days.
776
+ # since moved for many targeted syncs, Claude-side ones included (the git
777
+ # history of the constant is authoritative). The Codex-side ones are logged
778
+ # below: 2026-07-31 (gpt-5.6-terra/-luna correction), 2026-08-25
779
+ # (gpt-5.6-cyber), 2026-09-05 (gpt-6-astra), 2026-09-22 (gpt-6-sol,
780
+ # gpt-6-luna), 2026-09-23 (gpt-5.5-cyber), 2026-09-30 (gpt-6.1-sol) and
781
+ # 2026-11-05 (gpt-6.1-terra / gpt-6-rosalind / gpt-6-astra-law and the
782
+ # gpt-5.5-cyber-preview alias). Codex values outside the cards each of
783
+ # those syncs names were NOT re-verified on those days.
758
784
  # - As of the 2026-07-19 sync this carries every openai-provider
759
785
  # gpt-5* model the LiteLLM snapshot lists, so `pricing-check`'s scope finds
760
786
  # nothing missing. Models absent from this table still fall back to `gpt-5`
@@ -809,13 +835,32 @@ _unknown_model_warnings: set[str] = set()
809
835
  # existing gpt-5.6-cyber card.
810
836
  # 2026-09-23: adopted LiteLLM's historical gpt-5.5-cyber OpenAI-provider
811
837
  # card at $12.50 input / $1.25 cached input / $75.00 output per MTok.
812
- # The current OpenAI table lists its 5.6 successor instead. LiteLLM does
813
- # not publish an above-272K rate for 5.5 Cyber, so do not infer that tier.
838
+ # At that capture the OpenAI table listed only its 5.6 successor. LiteLLM
839
+ # does not publish an above-272K rate for 5.5 Cyber, so do not infer that
840
+ # tier. (2026-11-05: OpenAI's pricing page now lists gpt-5.5-cyber at the
841
+ # same $12.50 / $1.25 / $75.00 with dashes in the long-context columns, so
842
+ # the card is vendor-confirmed; values unchanged.)
814
843
  # 2026-09-29: added gpt-6.1-sol from OpenAI's model and pricing pages:
815
844
  # Standard $2.00 input / $0.10 cached input / $10.00 output per MTok;
816
845
  # above 272K, $4.00 / $0.20 / $15.00. Fast mode is 2x. LiteLLM does not
817
846
  # yet carry this identifier. The snapshot date advances to 2026-09-30
818
847
  # because the Sonnet 5.5 sync already used 2026-09-29 (#705).
848
+ # 2026-11-05 (#989): `pricing-check` reported four OpenAI-provider
849
+ # identifiers missing; each was checked against OpenAI's pricing page
850
+ # (https://developers.openai.com/api/docs/pricing) and LiteLLM, which agree.
851
+ # Added gpt-6.1-terra (Standard $1.00 input / $0.05 cached / $5.00 output
852
+ # from the pricing page; its model page states the above-272K rule, 2x
853
+ # input/cache and 1.5x output for the full request, giving $2.00 / $0.10 /
854
+ # $7.50, and 2x for Fast),
855
+ # gpt-6-rosalind (trusted-access life-sciences model, $15.00 / $1.50 /
856
+ # $75.00; above 272K $30.00 / $3.00 / $112.50) and gpt-6-astra-law ($10.00
857
+ # / $1.00 / $50.00, no long-context column; OpenAI marks this rate
858
+ # promotional "at least through December 31, 2026" and publishes no
859
+ # successor, so the only published rate is adopted and a later LiteLLM
860
+ # change will surface as value drift). gpt-5.5-cyber-preview has no vendor
861
+ # row and is aliased to gpt-5.5-cyber (see CODEX_MODEL_ALIASES). The fast
862
+ # multiplier of all three resolves to CODEX_FAST_MULTIPLIER_FALLBACK; the
863
+ # vendor publishes no Fast tier for Rosalind or Astra for Law.
819
864
  #
820
865
  # Billing rules:
821
866
  # - reasoning_output_tokens is billed at the *output* rate (matches
@@ -945,8 +990,10 @@ CODEX_MODEL_PRICING: dict[str, dict[str, Any]] = {
945
990
  # No cache_creation field: the Codex cost kernel never reads one, so
946
991
  # carrying LiteLLM's would only give `diff_pricing` a value to compare.
947
992
  "gpt-5.5-cyber": {
948
- # Historical OpenAI-provider card retained by LiteLLM; unlike the
949
- # 5.6 successor, no long-context fields are published for this ID.
993
+ # Adopted from LiteLLM's OpenAI-provider card; since confirmed by
994
+ # OpenAI's pricing page (2026-11-05, #989). Neither source publishes
995
+ # long-context rates for this ID, unlike LiteLLM's 5.6 successor card
996
+ # (OpenAI's page shows dashes for both).
950
997
  "input_cost_per_token": 1.25e-05,
951
998
  "cache_read_input_token_cost": 1.25e-06,
952
999
  "output_cost_per_token": 7.5e-05,
@@ -1001,6 +1048,41 @@ CODEX_MODEL_PRICING: dict[str, dict[str, Any]] = {
1001
1048
  "cache_read_input_token_cost_above_272k_tokens": 2e-08,
1002
1049
  "output_cost_per_token_above_272k_tokens": 7.5e-07,
1003
1050
  },
1051
+ "gpt-6.1-terra": {
1052
+ # Source: https://developers.openai.com/api/docs/models/gpt-6.1-terra
1053
+ # and /api/docs/pricing (#989). Standard $1.00/M input, $0.05/M cached
1054
+ # input (5% of input), $5.00/M output; above 272K input, 2x
1055
+ # input/cache and 1.5x output for the full request.
1056
+ "input_cost_per_token": 1e-06,
1057
+ "cache_read_input_token_cost": 5e-08,
1058
+ "output_cost_per_token": 5e-06,
1059
+ "input_cost_per_token_above_272k_tokens": 2e-06,
1060
+ "cache_read_input_token_cost_above_272k_tokens": 1e-07,
1061
+ "output_cost_per_token_above_272k_tokens": 7.5e-06,
1062
+ },
1063
+ "gpt-6-rosalind": {
1064
+ # Source: https://developers.openai.com/api/docs/pricing (#989), the
1065
+ # life-sciences table; access is limited to OpenAI's trusted-access
1066
+ # program. $15.00/M input, $1.50/M cached input, $75.00/M output;
1067
+ # long context $30.00 / $3.00 / $112.50.
1068
+ "input_cost_per_token": 1.5e-05,
1069
+ "cache_read_input_token_cost": 1.5e-06,
1070
+ "output_cost_per_token": 7.5e-05,
1071
+ "input_cost_per_token_above_272k_tokens": 3e-05,
1072
+ "cache_read_input_token_cost_above_272k_tokens": 3e-06,
1073
+ "output_cost_per_token_above_272k_tokens": 1.125e-04,
1074
+ },
1075
+ "gpt-6-astra-law": {
1076
+ # Source: https://developers.openai.com/api/docs/pricing (#989), the
1077
+ # legal table: $10.00/M input, $1.00/M cached input, $50.00/M output,
1078
+ # marked promotional "at least through December 31, 2026" with no
1079
+ # successor published. No long-context column is published (nor a
1080
+ # LiteLLM tier), so none is inferred and long requests price at these
1081
+ # base rates.
1082
+ "input_cost_per_token": 1e-05,
1083
+ "cache_read_input_token_cost": 1e-06,
1084
+ "output_cost_per_token": 5e-05,
1085
+ },
1004
1086
  # ── Issue #123: full gpt-5.x LiteLLM sync (2026-05-30 snapshot) ──
1005
1087
  # Exact model_prices_and_context_window.json values for every
1006
1088
  # openai-provider gpt-5* model `pricing-check`'s scope flags but the
@@ -1192,9 +1274,15 @@ CODEX_LEGACY_FALLBACK_MODEL = "gpt-5"
1192
1274
  # covered by a model-only PRICING_DRIFT_ALLOWLIST entry.
1193
1275
  # ``codex-auto-review`` is the hidden Guardian model and maps to the model
1194
1276
  # current when it appeared, covering every retained event observed for issue
1195
- # #535.
1277
+ # #535. ``gpt-5.5-cyber-preview`` (#989) is an INFERRED pricing equivalence,
1278
+ # not a vendor-declared alias: OpenAI publishes nothing for the identifier
1279
+ # (third-party press ties it to GPT-5.5-Cyber's limited preview), LiteLLM
1280
+ # prices it with cost fields
1281
+ # identical to ``gpt-5.5-cyber`` (observed 2026-11-05), and OpenAI lists that
1282
+ # card. Its model-only PRICING_DRIFT_ALLOWLIST entry covers the omission.
1196
1283
  CODEX_MODEL_ALIASES: dict[str, str] = {
1197
1284
  "codex-auto-review": "gpt-5.5",
1285
+ "gpt-5.5-cyber-preview": "gpt-5.5-cyber",
1198
1286
  "gpt-5.6": "gpt-5.6-sol",
1199
1287
  "gpt-daybreak-blue-latest": "gpt-5.6-sol",
1200
1288
  "gpt-daybreak-red-latest": "gpt-5.6-cyber",
@@ -1744,3 +1832,45 @@ def current_pricing_snapshot() -> PricingSnapshot:
1744
1832
  """The snapshot every cost kernel prices from; it never changes after
1745
1833
  import."""
1746
1834
  return _LIVE_PRICING_SNAPSHOT
1835
+
1836
+
1837
+ def codex_pricing_identity(snapshot: PricingSnapshot) -> str:
1838
+ """A hash of every Codex pricing semantic in ``snapshot`` (#1050 spec 4.7).
1839
+
1840
+ The Codex cycle-detail stamp carries it, because the detail prices each
1841
+ milestone at read time: the rate table alone is not every input — an alias
1842
+ re-points a runtime name at another card, the fallback model prices every
1843
+ unknown name, the tier threshold switches a row onto the long-context
1844
+ rates, and the fast multipliers scale the whole row. Claude's semantics are
1845
+ excluded so a Claude-only pricing edit leaves every Codex stamp alone.
1846
+
1847
+ Canonical JSON (sorted keys, no NaN): equal semantics hash equally whatever
1848
+ the dict insertion order.
1849
+ """
1850
+ payload = {
1851
+ "codex_pricing": snapshot.codex_pricing,
1852
+ "aliases": snapshot.aliases,
1853
+ "fallback_model": snapshot.fallback_model,
1854
+ "tier_threshold": snapshot.tier_thresholds.get("codex"),
1855
+ "fast_multipliers": {
1856
+ "codex": snapshot.fast_multipliers.get("codex"),
1857
+ "codex_fallback": snapshot.fast_multipliers.get("codex_fallback"),
1858
+ },
1859
+ }
1860
+ return hashlib.sha256(json.dumps(
1861
+ payload, sort_keys=True, separators=(",", ":"), allow_nan=False,
1862
+ ).encode("utf-8")).hexdigest()[:16]
1863
+
1864
+
1865
+ _LIVE_CODEX_PRICING_IDENTITY: "str | None" = None
1866
+
1867
+
1868
+ def live_codex_pricing_identity() -> str:
1869
+ """``codex_pricing_identity`` of the live snapshot, computed once per
1870
+ process: the snapshot never changes after import, and a newer binary is a
1871
+ new process (#868)."""
1872
+ global _LIVE_CODEX_PRICING_IDENTITY
1873
+ if _LIVE_CODEX_PRICING_IDENTITY is None:
1874
+ _LIVE_CODEX_PRICING_IDENTITY = codex_pricing_identity(
1875
+ current_pricing_snapshot())
1876
+ return _LIVE_CODEX_PRICING_IDENTITY
package/bin/_lib_quota.py CHANGED
@@ -256,6 +256,25 @@ class ResetAnchorComponents:
256
256
  def tolerance_seconds(self) -> int:
257
257
  return self._tolerance
258
258
 
259
+ def copy(self) -> "ResetAnchorComponents":
260
+ """An independent copy: mutating it never changes this index.
261
+
262
+ O(distinct raw resets). The provenance drain (#975, spec 4.2) starts
263
+ each identity from a copy of the committed-evidence base, so evidence
264
+ that identity resolves but does not store stays private to it.
265
+ """
266
+ other = object.__new__(type(self))
267
+ other._tolerance = self._tolerance
268
+ other._buckets = {
269
+ bucket: list(members) for bucket, members in self._buckets.items()}
270
+ other._parent = dict(self._parent)
271
+ other._rank = dict(self._rank)
272
+ other._anchor = dict(self._anchor)
273
+ other._first_order = dict(self._first_order)
274
+ other._member_order = dict(self._member_order)
275
+ other._next_order = self._next_order
276
+ return other
277
+
259
278
  def _bucket(self, value: dt.datetime) -> int:
260
279
  return int(value.timestamp() // self._tolerance)
261
280
 
@@ -3135,7 +3135,8 @@ def _reconcile_snapshot_identities(
3135
3135
  decision only when exact retained origins prove their causal order, axes,
3136
3136
  and dependency graph; otherwise leave the ordinary correction diff intact.
3137
3137
  """
3138
- if not isinstance(raw_observations, (list, tuple)):
3138
+ # #1004 D4: any sequence (a coordinate-backed one is read, never listed).
3139
+ if not isinstance(raw_observations, Sequence):
3139
3140
  raw_observations = tuple(raw_observations)
3140
3141
  reviewed_pairs = tuple(snapshot_identity_decisions or ())
3141
3142
  if not raw_observations and not reviewed_pairs:
@@ -3170,6 +3171,8 @@ def _reconcile_snapshot_identities(
3170
3171
  }
3171
3172
  if not needed_raw_ids:
3172
3173
  return
3174
+ # #1004 D4: positions only; a record is decoded again where it is read,
3175
+ # so the collection pass keeps no observation alive.
3173
3176
  raw_positions = {}
3174
3177
  repeated_raw_ids = set()
3175
3178
  for index, record in enumerate(raw_observations):
@@ -3183,7 +3186,7 @@ def _reconcile_snapshot_identities(
3183
3186
  if raw_id in raw_positions:
3184
3187
  repeated_raw_ids.add(raw_id)
3185
3188
  else:
3186
- raw_positions[raw_id] = (index, record)
3189
+ raw_positions[raw_id] = index
3187
3190
  # A repeated physical raw id has no unique order position. Leave its
3188
3191
  # accepted decision to the normal diff instead of choosing an occurrence.
3189
3192
  for raw_id in repeated_raw_ids:
@@ -3205,14 +3208,15 @@ def _reconcile_snapshot_identities(
3205
3208
  }
3206
3209
  candidates: dict[str, list[str]] = {}
3207
3210
  for old_id, old in unmatched_current.items():
3208
- old_index, old_raw = raw_positions[old_id[3:]]
3211
+ old_index = raw_positions[old_id[3:]]
3212
+ old_raw = raw_observations[old_index]
3209
3213
  candidates[old_id] = []
3210
3214
  for new_id, new in unmatched_desired.items():
3211
- new_index, new_raw = raw_positions[new_id[3:]]
3215
+ new_index = raw_positions[new_id[3:]]
3212
3216
  if (
3213
3217
  new_index < old_index
3214
3218
  and _same_acceptance_transition(
3215
- old, new, old_raw, new_raw,
3219
+ old, new, old_raw, raw_observations[new_index],
3216
3220
  raw_observations[new_index + 1:old_index],
3217
3221
  allow_intervening_same_account_observations=(
3218
3222
  (old_id[3:], new_id[3:]) in reviewed_set
@@ -3880,7 +3884,8 @@ def build_claude_usage_plan(*, selection, desired_events: Iterable[Mapping],
3880
3884
  and selected.record is not None
3881
3885
  and _is_owned_event(selected.record)
3882
3886
  }
3883
- if not isinstance(raw_observations, (list, tuple)):
3887
+ # #1004 D4: any sequence; a coordinate-backed one is never listed.
3888
+ if not isinstance(raw_observations, Sequence):
3884
3889
  raw_observations = tuple(raw_observations)
3885
3890
  _reconcile_snapshot_identities(
3886
3891
  current, desired, selection, raw_observations,