cctally 1.88.2 → 1.89.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.
- package/CHANGELOG.md +21 -0
- package/bin/_cctally_cache.py +827 -37
- package/bin/_cctally_config.py +125 -0
- package/bin/_cctally_core.py +86 -2
- package/bin/_cctally_dashboard_conversation.py +26 -7
- package/bin/_cctally_dashboard_sources.py +51 -0
- package/bin/_cctally_db.py +626 -0
- package/bin/_cctally_doctor.py +84 -1
- package/bin/_cctally_journal.py +2 -1
- package/bin/_cctally_parser.py +42 -0
- package/bin/_cctally_quota.py +1358 -112
- package/bin/_cctally_record.py +249 -9
- package/bin/_cctally_setup.py +14 -5
- package/bin/_cctally_store.py +16 -1
- package/bin/_cctally_tui.py +16 -2
- package/bin/_cctally_update.py +9 -2
- package/bin/_lib_background_mcp.py +168 -0
- package/bin/_lib_codex_conversation.py +8 -0
- package/bin/_lib_codex_conversation_query.py +8 -7
- package/bin/_lib_conversation.py +105 -5
- package/bin/_lib_conversation_dispatch.py +15 -4
- package/bin/_lib_conversation_query.py +294 -2
- package/bin/_lib_dashboard_sources.py +5 -1
- package/bin/_lib_doctor.py +202 -1
- package/bin/_lib_jsonl.py +12 -0
- package/bin/_lib_quota_alert_axes.py +188 -0
- package/bin/_lib_quota_ledger.py +274 -0
- package/bin/_lib_snapshot_cache.py +36 -0
- package/bin/cctally +6 -3
- package/dashboard/static/assets/{index-Bvp8mxtz.js → index-B2exY5DG.js} +52 -52
- package/dashboard/static/assets/{index-B0ZCsoxI.css → index-DsXnVi9J.css} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +4 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Pure kernel for the Codex quota change ledger (public issue omrikais/cctally#5).
|
|
2
|
+
|
|
3
|
+
Spec:
|
|
4
|
+
``docs/superpowers/specs/2026-07-31-codex-hook-incremental-quota-reconcile-design.md``
|
|
5
|
+
§1-§2.
|
|
6
|
+
|
|
7
|
+
Three key spaces meet here and confusing any two of them is silent:
|
|
8
|
+
|
|
9
|
+
* **Raw coordinates** are what the SQLite triggers record and what the loader's
|
|
10
|
+
exact filter matches: the stored ``(source_root_key, logical_limit_key,
|
|
11
|
+
observed_slot, window_minutes, canonical-or-raw reset)`` of one row image.
|
|
12
|
+
Nothing is interpreted, because interpretation is population-dependent (the
|
|
13
|
+
account fold reads every observation of a window) and cannot be expressed in
|
|
14
|
+
SQL at all.
|
|
15
|
+
|
|
16
|
+
* **The loading unit** is the raw group closed over the ``window_minutes`` snap.
|
|
17
|
+
The provider occasionally reports a weekly window as ``10081``, and that one
|
|
18
|
+
minute lives in BOTH the limit key and a column of its own, so two raw groups
|
|
19
|
+
can interpret into one window. Loading a raw group alone would then hand the
|
|
20
|
+
projector a PARTIAL population — a wrong block, not a stale one. The loading
|
|
21
|
+
unit is also the reverse map persisted as ``quota_window_blocks.
|
|
22
|
+
physical_group_key`` and the unit the per-group digest is taken over.
|
|
23
|
+
|
|
24
|
+
* **The interpreted identity** is what ``QuotaObservation`` carries after the
|
|
25
|
+
read path snaps the length and rewrites the limit key for a model-scoped pool.
|
|
26
|
+
One loading unit can contain several interpreted identities (a Spark window
|
|
27
|
+
and an ordinary one), which is fine and deliberate: the unit is a partition of
|
|
28
|
+
the root's observations, so loading it whole gives every identity inside it
|
|
29
|
+
its complete population.
|
|
30
|
+
|
|
31
|
+
The bridge between the last two is ``strip_model_pool``. A model-scoped
|
|
32
|
+
interpreted key is the ordinary key with a ``modelPool`` member added, so
|
|
33
|
+
removing that member recovers the loading unit's key — which is what lets a
|
|
34
|
+
block stamp its own unit without the loader having to carry raw coordinates
|
|
35
|
+
alongside every observation.
|
|
36
|
+
|
|
37
|
+
Everything here is pure: no SQLite, no clock, no I/O.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import datetime as dt
|
|
42
|
+
import hashlib
|
|
43
|
+
import json
|
|
44
|
+
from typing import Iterable, Mapping, Sequence
|
|
45
|
+
|
|
46
|
+
from _lib_jsonl import (
|
|
47
|
+
_codex_canonical_json,
|
|
48
|
+
codex_snap_equivalent_limit_keys,
|
|
49
|
+
codex_snap_equivalent_window_minutes,
|
|
50
|
+
snap_codex_window_minutes,
|
|
51
|
+
snap_window_minutes,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
#: The ledger's row-image column suffixes, in group order.
|
|
56
|
+
LEDGER_GROUP_SUFFIXES = (
|
|
57
|
+
"source_root_key",
|
|
58
|
+
"logical_limit_key",
|
|
59
|
+
"observed_slot",
|
|
60
|
+
"window_minutes",
|
|
61
|
+
"resets_at_utc",
|
|
62
|
+
"canonical_resets_at_utc",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
#: Field separator for the serialized group key. A unit separator cannot occur
|
|
66
|
+
#: in a canonical-JSON limit key, an ISO timestamp, a slot name or a root key,
|
|
67
|
+
#: so the join is unambiguous without escaping.
|
|
68
|
+
_KEY_SEPARATOR = "\x1f"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _text(value: object) -> str | None:
|
|
72
|
+
if value is None:
|
|
73
|
+
return None
|
|
74
|
+
text = str(value)
|
|
75
|
+
return text if text.strip() else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _side_group(row: Mapping[str, object], prefix: str) -> tuple | None:
|
|
79
|
+
"""One row image's raw group, or ``None`` when it cannot form one.
|
|
80
|
+
|
|
81
|
+
A side with no root, slot, limit key, length or reset is a row the loader
|
|
82
|
+
would skip anyway (its required-text guard drops it before it can become a
|
|
83
|
+
quota identity), so there is nothing for the projector to expand.
|
|
84
|
+
"""
|
|
85
|
+
root = _text(row.get(prefix + "source_root_key"))
|
|
86
|
+
limit_key = _text(row.get(prefix + "logical_limit_key"))
|
|
87
|
+
slot = _text(row.get(prefix + "observed_slot"))
|
|
88
|
+
minutes = row.get(prefix + "window_minutes")
|
|
89
|
+
# COALESCE, exactly as the loader's predicate does: a row whose anchor was
|
|
90
|
+
# never resolved falls back to its raw reset on every read path, so the
|
|
91
|
+
# group coordinate has to fall back with it.
|
|
92
|
+
reset = (
|
|
93
|
+
_text(row.get(prefix + "canonical_resets_at_utc"))
|
|
94
|
+
or _text(row.get(prefix + "resets_at_utc"))
|
|
95
|
+
)
|
|
96
|
+
if None in (root, limit_key, slot, reset) or minutes is None:
|
|
97
|
+
return None
|
|
98
|
+
try:
|
|
99
|
+
minutes_int = int(minutes)
|
|
100
|
+
except (TypeError, ValueError):
|
|
101
|
+
return None
|
|
102
|
+
return (root, limit_key, slot, minutes_int, reset)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def expand_dirty_groups(
|
|
106
|
+
ledger_rows: Iterable[Mapping[str, object]],
|
|
107
|
+
) -> frozenset[tuple]:
|
|
108
|
+
"""Map ledger entries to the union of their old and new raw group coordinates.
|
|
109
|
+
|
|
110
|
+
Both sides, always. A semantic UPDATE can move rows BETWEEN groups, so the
|
|
111
|
+
pass has to re-materialize the new group AND sweep the old one — a group
|
|
112
|
+
that has lost all its members is swept to nothing, and that only works if
|
|
113
|
+
the sweep knows where the rows came from. An insert contributes only a new
|
|
114
|
+
side and a delete only an old one, which falls out of the same rule rather
|
|
115
|
+
than needing a case per op.
|
|
116
|
+
"""
|
|
117
|
+
groups: set[tuple] = set()
|
|
118
|
+
for row in ledger_rows:
|
|
119
|
+
for prefix in ("old_", "new_"):
|
|
120
|
+
group = _side_group(row, prefix)
|
|
121
|
+
if group is not None:
|
|
122
|
+
groups.add(group)
|
|
123
|
+
return frozenset(groups)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def snap_equivalent_raw_groups(groups: Iterable[tuple]) -> frozenset[tuple]:
|
|
127
|
+
"""Close a raw group set over the ``window_minutes`` snap.
|
|
128
|
+
|
|
129
|
+
Rows are STORED under whichever spelling the provider sent, so the exact
|
|
130
|
+
filter has to ask for each of them by name. The set is bounded at nine per
|
|
131
|
+
input group (three equivalent limit keys x three equivalent lengths) and
|
|
132
|
+
contains the input itself, so a non-snappable group resolves to exactly
|
|
133
|
+
itself. Asking for a combination no row carries costs one indexed miss.
|
|
134
|
+
|
|
135
|
+
The cross product rather than a zip is deliberate: a key whose
|
|
136
|
+
``windowMinutes`` member disagrees with the column (only reachable by hand
|
|
137
|
+
repair) would otherwise fall outside its own closure.
|
|
138
|
+
"""
|
|
139
|
+
widened: set[tuple] = set()
|
|
140
|
+
for root, limit_key, slot, minutes, reset in groups:
|
|
141
|
+
for key in codex_snap_equivalent_limit_keys(limit_key):
|
|
142
|
+
for equivalent in codex_snap_equivalent_window_minutes(minutes):
|
|
143
|
+
widened.add((root, key, slot, int(equivalent), reset))
|
|
144
|
+
return frozenset(widened)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def strip_model_pool(logical_limit_key: str) -> str:
|
|
148
|
+
"""Return the limit key with any ``modelPool`` member removed.
|
|
149
|
+
|
|
150
|
+
A model-scoped interpreted key is the ordinary key plus that member, so
|
|
151
|
+
dropping it recovers the loading unit's key — the value a ledger entry's raw
|
|
152
|
+
coordinates snap to. Fails OPEN on shape: a key this cannot parse is
|
|
153
|
+
returned exactly as it arrived rather than rebuilt from guessed members,
|
|
154
|
+
which keeps a hand-written or legacy key comparing equal to itself.
|
|
155
|
+
"""
|
|
156
|
+
if not isinstance(logical_limit_key, str):
|
|
157
|
+
return logical_limit_key
|
|
158
|
+
try:
|
|
159
|
+
payload = json.loads(logical_limit_key)
|
|
160
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
161
|
+
return logical_limit_key
|
|
162
|
+
if not isinstance(payload, dict) or "modelPool" not in payload:
|
|
163
|
+
return logical_limit_key
|
|
164
|
+
payload.pop("modelPool")
|
|
165
|
+
try:
|
|
166
|
+
return _codex_canonical_json(payload)
|
|
167
|
+
except (TypeError, ValueError): # pragma: no cover - non-serializable member
|
|
168
|
+
return logical_limit_key
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def normalize_reset(value: object) -> str:
|
|
172
|
+
"""One spelling for one instant.
|
|
173
|
+
|
|
174
|
+
The cache retains whatever the provider sent — ``2026-07-01T05:00:00Z`` from
|
|
175
|
+
one writer, ``…+00:00`` from another — and every SQL comparison in this
|
|
176
|
+
subsystem wraps the column in ``unixepoch()`` for exactly that reason. The
|
|
177
|
+
loading-unit key is a TEXT key compared with ``=``, so it has no such
|
|
178
|
+
escape: a ledger entry naming the ``Z`` spelling and a block stamping the
|
|
179
|
+
``+00:00`` one would be two different units, the scoped sweep would look for
|
|
180
|
+
a key nothing wrote, and a vanished window's block would survive.
|
|
181
|
+
|
|
182
|
+
Fails OPEN: a value this cannot parse is returned as text, so it still
|
|
183
|
+
compares equal to itself.
|
|
184
|
+
"""
|
|
185
|
+
text = str(value)
|
|
186
|
+
try:
|
|
187
|
+
parsed = dt.datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
188
|
+
except ValueError:
|
|
189
|
+
return text
|
|
190
|
+
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
|
191
|
+
return text
|
|
192
|
+
return parsed.astimezone(dt.timezone.utc).isoformat()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def loading_unit_from_raw(group: Sequence) -> tuple:
|
|
196
|
+
"""The loading unit one raw group belongs to."""
|
|
197
|
+
root, limit_key, slot, minutes, reset = group
|
|
198
|
+
return (
|
|
199
|
+
str(root),
|
|
200
|
+
snap_window_minutes(str(limit_key)),
|
|
201
|
+
str(slot),
|
|
202
|
+
int(snap_codex_window_minutes(int(minutes))),
|
|
203
|
+
normalize_reset(reset),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def loading_unit_from_identity(
|
|
208
|
+
*,
|
|
209
|
+
source_root_key: str,
|
|
210
|
+
logical_limit_key: str,
|
|
211
|
+
observed_slot: str,
|
|
212
|
+
window_minutes: int,
|
|
213
|
+
canonical_reset_iso: str,
|
|
214
|
+
) -> tuple:
|
|
215
|
+
"""The loading unit an INTERPRETED identity belongs to.
|
|
216
|
+
|
|
217
|
+
The identity's length is already snapped and its key already carries any
|
|
218
|
+
model pool, so the only transform left is removing that pool member. Both
|
|
219
|
+
this and ``loading_unit_from_raw`` must land on the same value for the same
|
|
220
|
+
physical rows — that identity is what lets a block record its own unit while
|
|
221
|
+
a ledger entry names the same unit from raw coordinates alone.
|
|
222
|
+
"""
|
|
223
|
+
return (
|
|
224
|
+
str(source_root_key),
|
|
225
|
+
strip_model_pool(str(logical_limit_key)),
|
|
226
|
+
str(observed_slot),
|
|
227
|
+
int(window_minutes),
|
|
228
|
+
normalize_reset(canonical_reset_iso),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def physical_group_key_text(unit: Sequence) -> str:
|
|
233
|
+
"""Serialize a loading unit for storage and for an SQL ``IN`` match."""
|
|
234
|
+
root, limit_key, slot, minutes, reset = unit
|
|
235
|
+
return _KEY_SEPARATOR.join(
|
|
236
|
+
(str(root), str(limit_key), str(slot), str(int(minutes)), str(reset)))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def group_digest(tuples: Iterable[Sequence]) -> str:
|
|
240
|
+
"""Digest one loading unit's observation tuples.
|
|
241
|
+
|
|
242
|
+
The tuple shape is the whole-root signature's, unchanged, so a group's
|
|
243
|
+
contribution is exactly the rows it owns.
|
|
244
|
+
"""
|
|
245
|
+
encoded = json.dumps(
|
|
246
|
+
sorted([list(item) for item in tuples]),
|
|
247
|
+
ensure_ascii=False, separators=(",", ":"),
|
|
248
|
+
).encode("utf-8")
|
|
249
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def compose_root_signature(pairs: Iterable[tuple[str, str]]) -> str:
|
|
253
|
+
"""Compose a root's physical signature from its per-group digests.
|
|
254
|
+
|
|
255
|
+
This is what makes the signature ASSOCIATIVE. The previous value was a
|
|
256
|
+
sha256 over the sorted per-observation tuples of a whole root, which a
|
|
257
|
+
bounded pass physically cannot reproduce — and the measured alternatives
|
|
258
|
+
were already rejected at 0.70s exact and 0.23s inexact against a 500ms
|
|
259
|
+
budget. Digesting the root's sorted ``(group key, group digest)`` pairs
|
|
260
|
+
instead is O(groups): 608 on the real store, against 211K observations.
|
|
261
|
+
|
|
262
|
+
The VALUE differs from the old whole-root digest; the SEMANTICS do not. It
|
|
263
|
+
stays an exact-equality function of the physical evidence alone, so
|
|
264
|
+
``_stats_projection_signatures_match`` and the cache certificate keep meaning
|
|
265
|
+
what they mean today, and a bounded pass and a whole-history pass over the
|
|
266
|
+
same store must produce the same string. The transition is covered by the
|
|
267
|
+
interpretation-version bump, which invalidates every certificate written
|
|
268
|
+
under the old scheme.
|
|
269
|
+
"""
|
|
270
|
+
encoded = json.dumps(
|
|
271
|
+
sorted({(str(key), str(digest)) for key, digest in pairs}),
|
|
272
|
+
ensure_ascii=False, separators=(",", ":"),
|
|
273
|
+
).encode("utf-8")
|
|
274
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
@@ -45,6 +45,7 @@ reason.
|
|
|
45
45
|
from __future__ import annotations
|
|
46
46
|
|
|
47
47
|
import datetime as dt
|
|
48
|
+
import hashlib
|
|
48
49
|
import os
|
|
49
50
|
import sqlite3
|
|
50
51
|
import threading
|
|
@@ -97,6 +98,14 @@ class SnapshotSignature(NamedTuple):
|
|
|
97
98
|
# an account SWITCH with zero new ingested rows, so the idle short-circuit is
|
|
98
99
|
# left and the `active` marker rebuilds on the next tick.
|
|
99
100
|
accounts_digest: str = ""
|
|
101
|
+
# public #5: the hook's budgeted-ingest backlog record, which feeds the
|
|
102
|
+
# Codex envelope's `ingest_backlog` field. A tick whose budgeted walk
|
|
103
|
+
# consumed only deduped or non-`token_count` bytes moves NO other cache leg,
|
|
104
|
+
# so without this the backlog changes with nothing to publish it and the
|
|
105
|
+
# hero's "totals will rise" note goes stale or missing until some unrelated
|
|
106
|
+
# Codex mutation happens along. Empty when nothing is owed (the writer
|
|
107
|
+
# DELETEs the key at zero), so a fully-ingested store is byte-neutral.
|
|
108
|
+
codex_ingest_backlog_sig: str = ""
|
|
100
109
|
|
|
101
110
|
|
|
102
111
|
def _max_id(conn: sqlite3.Connection, table: str) -> int:
|
|
@@ -156,6 +165,32 @@ def _codex_physical_mutation_seq(conn: sqlite3.Connection) -> int:
|
|
|
156
165
|
return 0
|
|
157
166
|
|
|
158
167
|
|
|
168
|
+
def _codex_ingest_backlog_sig(conn: sqlite3.Connection) -> str:
|
|
169
|
+
"""Digest of the ``codex_ingest_backlog`` record, ``""`` when absent (#5).
|
|
170
|
+
|
|
171
|
+
The same O(1) ``cache_meta`` KV read as the mutation counters above — one
|
|
172
|
+
primary-key lookup on a table with a handful of rows, so this stays off the
|
|
173
|
+
per-tick cost that #268 removed.
|
|
174
|
+
|
|
175
|
+
A DIGEST rather than the parsed counts: the whole record is what the
|
|
176
|
+
envelope publishes (``files``, ``bytes`` and the one-hour ``since`` clock),
|
|
177
|
+
and hashing the stored bytes covers every field including any added later,
|
|
178
|
+
without teaching the signature the record's shape. The writer DELETEs the
|
|
179
|
+
key once the backlog drains, so ``""`` is both "never had one" and
|
|
180
|
+
"finished" — which is what keeps the version string byte-identical to the
|
|
181
|
+
pre-#5 one for the overwhelming majority of installs.
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
row = conn.execute(
|
|
185
|
+
"SELECT value FROM cache_meta WHERE key='codex_ingest_backlog'"
|
|
186
|
+
).fetchone()
|
|
187
|
+
except sqlite3.Error:
|
|
188
|
+
return ""
|
|
189
|
+
if row is None or not row[0]:
|
|
190
|
+
return ""
|
|
191
|
+
return hashlib.sha256(str(row[0]).encode("utf-8")).hexdigest()[:16]
|
|
192
|
+
|
|
193
|
+
|
|
159
194
|
def _reset_sig(conn: sqlite3.Connection) -> tuple[int, int]:
|
|
160
195
|
"""Change-signal over the two reset-event tables combined (spec §3).
|
|
161
196
|
|
|
@@ -207,6 +242,7 @@ def compute_signature(
|
|
|
207
242
|
codex_physical_mutation_seq=_codex_physical_mutation_seq(cache_conn),
|
|
208
243
|
codex_stats_digest=str(codex_stats_digest),
|
|
209
244
|
accounts_digest=str(accounts_digest),
|
|
245
|
+
codex_ingest_backlog_sig=_codex_ingest_backlog_sig(cache_conn),
|
|
210
246
|
)
|
|
211
247
|
|
|
212
248
|
|
package/bin/cctally
CHANGED
|
@@ -1053,6 +1053,7 @@ sync_claude_conversations = _cctally_cache.sync_claude_conversations
|
|
|
1053
1053
|
sync_codex_conversations = _cctally_cache.sync_codex_conversations
|
|
1054
1054
|
_reset_orphan_warning_throttle = _cctally_cache._reset_orphan_warning_throttle
|
|
1055
1055
|
cmd_cache_sync = _cctally_cache.cmd_cache_sync
|
|
1056
|
+
cmd_codex_replay_drain_internal = _cctally_cache.cmd_codex_replay_drain_internal
|
|
1056
1057
|
|
|
1057
1058
|
|
|
1058
1059
|
# Record-usage / hook-tick hot-path subsystem — the runtime path that
|
|
@@ -1452,6 +1453,7 @@ cmd_codex_session = _cctally_codex.cmd_codex_session
|
|
|
1452
1453
|
# adapter while retaining the existing Codex accounting commands unchanged.
|
|
1453
1454
|
_cctally_quota = _load_sibling("_cctally_quota")
|
|
1454
1455
|
reconcile_codex_quota_projection = _cctally_quota.reconcile_codex_quota_projection
|
|
1456
|
+
cmd_codex_quota_verify_internal = _cctally_quota.cmd_codex_quota_verify_internal
|
|
1455
1457
|
cmd_codex_quota_history = _cctally_quota.cmd_codex_quota_history
|
|
1456
1458
|
cmd_codex_quota_statusline = _cctally_quota.cmd_codex_quota_statusline
|
|
1457
1459
|
cmd_codex_quota_forecast = _cctally_quota.cmd_codex_quota_forecast
|
|
@@ -3457,9 +3459,10 @@ def _post_command_update_hooks(command: str | None, args) -> None:
|
|
|
3457
3459
|
# (it runs before the wrapper sets CCTALLY_DATA_DIR, so APP_DIR is
|
|
3458
3460
|
# still cctally-dev at this point). Same rationale class as doctor.
|
|
3459
3461
|
return
|
|
3460
|
-
if command in ("_update-check", "_telemetry-beat"
|
|
3461
|
-
|
|
3462
|
-
#
|
|
3462
|
+
if command in ("_update-check", "_telemetry-beat", "_codex-quota-verify",
|
|
3463
|
+
"_codex-replay-drain"):
|
|
3464
|
+
# All four hidden workers are detached and have already done their one job
|
|
3465
|
+
# in their own command handler; none must re-enter this hook.
|
|
3463
3466
|
# Without the guard the ``_update-check`` worker would fall through to
|
|
3464
3467
|
# the telemetry gate below and (throttle-bounded) spawn a
|
|
3465
3468
|
# ``_telemetry-beat`` — a worker spawning another worker. And
|