recency_guard module + wired propagate_stack_now + top20 (Tony 2026-07-13)
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
# Athena AI News — Top 20 (Monday Week-Open, Today Only)
|
||||||
|
|
||||||
|
> Generated 2026-07-13 20:54 UTC · recency-guard ON · GitHub EXCLUDED (standing rule) · ranked by virality × recency × editorial fit
|
||||||
|
> Source: oracle.db · 20 shown of 129 fresh today (-GitHub) · 98 older re-posts dropped
|
||||||
|
|
||||||
|
| # | Title | Source | Score | Age | Link |
|
||||||
|
|---|-------|--------|-------|-----|------|
|
||||||
|
| 1 | Zig Creator Calls Spade a Spade, Anthropic Blows Smoke | hackernews | 0.573 | today | [link](https://raymyers.org/post/zed-creator-calls-spade-a-spade/) |
|
||||||
|
| 2 | Grok uploaded my user directory to xAI's servers | hackernews | 0.527 | today | [link](https://twitter.com/a_green_being/status/2076598897779020159) |
|
||||||
|
| 3 | Ask HN: Add flag for AI-generated articles | hackernews | 0.519 | today | [link](https://news.ycombinator.com/item/48886741) |
|
||||||
|
| 4 | I love LLMs, I hate hype | hackernews | 0.468 | today | [link](https://geohot.github.io//blog/jekyll/update/2026/07/12/i-love-llms.html) |
|
||||||
|
| 5 | What xAI's Grok build CLI sends to xAI: A wire-level analysis | hackernews | 0.442 | today | [link](https://gist.github.com/cereblab/dc9a40bc26120f4540e4e09b75ffb547) |
|
||||||
|
| 6 | Show HN: Clawk – Give coding agents a disposable Linux VM, not your laptop | hackernews | 0.437 | today | [link](https://github.com/clawkwork/clawk) |
|
||||||
|
| 7 | Old and new apps, via modern coding agents | hackernews | 0.435 | today | [link](https://terrytao.wordpress.com/2026/07/11/old-and-new-apps-via-modern-coding-agents/) |
|
||||||
|
| 8 | xAI's Grok Build CLI Uploads Git Repositories to a Google Cloud Bucket | hackernews | 0.433 | today | [link](https://www.internationalcyberdigest.com/xais-grok-build-cli-uploads-entire-git-repositories-to-a-google-cloud-bucket/) |
|
||||||
|
| 9 | Migrating a production AI agent to GPT-5.6: 2.2x faster, 27% cheaper | hackernews | 0.420 | today | [link](https://ploy.ai/blog/migrating-a-production-ai-agent-to-gpt-5-6) |
|
||||||
|
| 10 | Mesh LLM: distributed AI computing on iroh | hackernews | 0.394 | today | [link](https://www.iroh.computer/blog/mesh-llm) |
|
||||||
|
| 11 | AI boosts research careers but narrow the span of ideas explored: study | hackernews | 0.358 | today | [link](https://spectrum.ieee.org/ai-science-research-flattens-discovery) |
|
||||||
|
| 12 | Stop Telling Me to Ask an LLM | hackernews | 0.355 | today | [link](https://blog.yaelwrites.com/stop-telling-me-to-ask-an-llm/) |
|
||||||
|
| 13 | Show HN: Mindwalk – Replay coding-agent sessions on a 3D map of your codebase | hackernews | 0.340 | today | [link](https://github.com/cosmtrek/mindwalk) |
|
||||||
|
| 14 | Mechanistic interpretability researchers applying causality theory to LLMs | hackernews | 0.338 | today | [link](https://cacm.acm.org/news/can-we-understand-how-large-language-models-reason/) |
|
||||||
|
| 15 | AI 2040 and the cult of intelligence | hackernews | 0.297 | today | [link](https://geohot.github.io//blog/jekyll/update/2026/07/11/ai-2040.html) |
|
||||||
|
| 16 | The One-Step Trap (In AI Research) | hackernews | 0.263 | today | [link](http://incompleteideas.net/IncIdeas/OneStepTrap.html) |
|
||||||
|
| 17 | Wealthy AI workers send San Francisco house prices soaring | hackernews | 0.258 | today | [link](https://www.bbc.com/news/articles/c9q29j47v9ro) |
|
||||||
|
| 18 | Reverse centaurs are the answer to the AI paradox (2025) | hackernews | 0.250 | today | [link](https://pluralistic.net/2025/09/11/vulgar-thatcherism/#there-is-an-alternative) |
|
||||||
|
| 19 | Who manages the agents? | hackernews | 0.240 | today | [link](https://www.off-policy.com/dont-go-quietly-into-the-ai-night/) |
|
||||||
|
| 20 | Show HN: Reame – a CPU inference server that gets faster as it runs | hackernews | 0.195 | today | [link](https://github.com/swellweb/reame) |
|
||||||
|
|
||||||
|
## Editorial fit tags
|
||||||
|
|
||||||
|
2. **Grok uploaded my user directory to xAI's servers** — model-wars, privacy/sec
|
||||||
|
4. **I love LLMs, I hate hype** — anti-hype
|
||||||
|
5. **What xAI's Grok build CLI sends to xAI: A wire-level analysis** — model-wars
|
||||||
|
6. **Show HN: Clawk – Give coding agents a disposable Linux VM, not your la** — agents
|
||||||
|
7. **Old and new apps, via modern coding agents** — agents
|
||||||
|
8. **xAI's Grok Build CLI Uploads Git Repositories to a Google Cloud Bucket** — model-wars, privacy/sec, local/infra
|
||||||
|
9. **Migrating a production AI agent to GPT-5.6: 2.2x faster, 27% cheaper** — agents, model-wars
|
||||||
|
10. **Mesh LLM: distributed AI computing on iroh** — local/infra
|
||||||
|
11. **AI boosts research careers but narrow the span of ideas explored: stud** — biz/legal
|
||||||
|
13. **Show HN: Mindwalk – Replay coding-agent sessions on a 3D map of your c** — agents
|
||||||
|
14. **Mechanistic interpretability researchers applying causality theory to ** — local/infra
|
||||||
|
16. **The One-Step Trap (In AI Research)** — biz/legal, anti-hype
|
||||||
|
18. **Reverse centaurs are the answer to the AI paradox (2025)** — anti-hype
|
||||||
|
19. **Who manages the agents?** — agents
|
||||||
|
20. **Show HN: Reame – a CPU inference server that gets faster as it runs** — local/infra
|
||||||
+19
-17
@@ -201,22 +201,22 @@ def _get_half_life(item):
|
|||||||
# spec, shorter than evergreen). Covers both manual curations and
|
# spec, shorter than evergreen). Covers both manual curations and
|
||||||
# auto-classified section items, so a section link persists 14 days
|
# auto-classified section items, so a section link persists 14 days
|
||||||
# instead of sinking in ~18h. Beyond this window the item is routed to
|
# instead of sinking in ~18h. Beyond this window the item is routed to
|
||||||
# Archive (generator).
|
def _get_half_life(item):
|
||||||
ms = (item.get("manual_section") or "").upper()
|
"""Return section/tier-specific half-life in hours, or None to fall back
|
||||||
if ms in ("HARDWARE", "TIPS"):
|
to decay_index's half_life_h argument (default 18h news half-life)."""
|
||||||
return 336.0
|
# Spec v1 addendum: Hardware/Tips section items get a 14-day (336h)
|
||||||
sec = (item.get("computed_section") or "").upper()
|
# half-life so curated/section items persist far longer in-section.
|
||||||
if sec in ("HARDWARE", "TIPS"):
|
ms = (item.get("manual_section") or "").upper()
|
||||||
return 336.0
|
if ms in ("HARDWARE", "TIPS"):
|
||||||
tier = item.get("tier", "normal")
|
return 336.0
|
||||||
# Tier overrides take precedence
|
tier = item.get("tier", "normal")
|
||||||
if tier == "breaking":
|
# Tier overrides take precedence
|
||||||
return CATEGORY_HALF_LIVES["breaking"]
|
if tier == "breaking":
|
||||||
if tier == "update":
|
return CATEGORY_HALF_LIVES["breaking"]
|
||||||
return CATEGORY_HALF_LIVES["update"]
|
if tier == "update":
|
||||||
# Otherwise use content_type
|
return CATEGORY_HALF_LIVES["update"]
|
||||||
ct = item.get("content_type", "OTHER")
|
# No special case -> let decay_index use its half_life_h parameter.
|
||||||
return CATEGORY_HALF_LIVES.get(ct, CATEGORY_HALF_LIVES["OTHER"])
|
return None
|
||||||
|
|
||||||
|
|
||||||
def decay_index(items, half_life_h=18.0):
|
def decay_index(items, half_life_h=18.0):
|
||||||
@@ -235,8 +235,10 @@ def decay_index(items, half_life_h=18.0):
|
|||||||
age = _age_hours(it)
|
age = _age_hours(it)
|
||||||
it["age_hours"] = round(age, 1)
|
it["age_hours"] = round(age, 1)
|
||||||
base = it.get("clickability", 0) or 0
|
base = it.get("clickability", 0) or 0
|
||||||
# Category-specific half-life
|
# Category-specific half-life, falling back to the passed half_life_h
|
||||||
hl = _get_half_life(it)
|
hl = _get_half_life(it)
|
||||||
|
if hl is None:
|
||||||
|
hl = half_life_h
|
||||||
k = math.log(2) / hl
|
k = math.log(2) / hl
|
||||||
it["clickability_decayed"] = round(base * math.exp(-k * age), 4)
|
it["clickability_decayed"] = round(base * math.exp(-k * age), 4)
|
||||||
it["effective_half_life"] = hl
|
it["effective_half_life"] = hl
|
||||||
|
|||||||
@@ -104,6 +104,15 @@ def main():
|
|||||||
and it.get("first_seen") and _parse(it["first_seen"])
|
and it.get("first_seen") and _parse(it["first_seen"])
|
||||||
and _parse(it["first_seen"]) >= cutoff
|
and _parse(it["first_seen"]) >= cutoff
|
||||||
and (it.get("source") or "").lower() not in EXCLUDE_SOURCES]
|
and (it.get("source") or "").lower() not in EXCLUDE_SOURCES]
|
||||||
|
# --- RECENCY GUARD (2026-07-13, Tony's correction): NEVER re-post old news.
|
||||||
|
# Age is the dominant measure: today's items ALWAYS lead (week-open fresh
|
||||||
|
# news); older items keep only if NEVER posted before (md-stack / seen).
|
||||||
|
# This kills the Apple-vs-OpenAI / GPT-5.6 re-post problem at the source. ---
|
||||||
|
from recency_guard import filter_fresh as _rg_filter
|
||||||
|
_today, _older_new, _dropped = _rg_filter(eligible)
|
||||||
|
if _dropped:
|
||||||
|
print(f"[recency_guard] dropped {len(_dropped)} already-posted older items")
|
||||||
|
eligible = _today + _older_new
|
||||||
eligible.sort(key=lambda x: x["clickability_decayed"], reverse=True)
|
eligible.sort(key=lambda x: x["clickability_decayed"], reverse=True)
|
||||||
top = eligible[:LIMIT]
|
top = eligible[:LIMIT]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
recency_guard.py — Athena "how old is this news?" gate (Tony's correction, 2026-07-13).
|
||||||
|
|
||||||
|
THE PROBLEM IT SOLVES:
|
||||||
|
The old pipeline ranked by virality + editorial fit ONLY. That surfaced the
|
||||||
|
SAME stories week after week (Apple-vs-OpenAI, GPT-5.6, etc.) because the
|
||||||
|
curation had NO memory of what was already posted. Tony's rule:
|
||||||
|
|
||||||
|
"when you receive the news stories, morality [editorial fit] is only ONE
|
||||||
|
measure. The OTHER measure -- maybe MORE important -- is how OLD is the
|
||||||
|
news. Today is Monday; the week's news is just beginning, so we begin the
|
||||||
|
week with stories dated for TODAY."
|
||||||
|
|
||||||
|
So AGE is the dominant gate. The algorithm (editorial, not render-time):
|
||||||
|
|
||||||
|
* TODAY's items -> ALWAYS eligible. They are this week's fresh news; a
|
||||||
|
Monday stack leads with them even if rendered earlier today.
|
||||||
|
* OLDER items -> eligible ONLY if NEVER posted before (not in the
|
||||||
|
markdown Top-N stack history AND not in seen_urls.json).
|
||||||
|
This kills the re-post problem at the source.
|
||||||
|
|
||||||
|
WHAT IS "ALREADY POSTED":
|
||||||
|
Two signals, differing in authority:
|
||||||
|
- athena_top*.md = the CURATED/PUBLIC stack history (authoritative)
|
||||||
|
- seen_urls.json = the live-site render dedup (secondary; ALSO flags items
|
||||||
|
rendered in prior runs TODAY, which we must NOT drop)
|
||||||
|
Because seen_urls.json contains today's own items, we only treat a seen_url
|
||||||
|
as "already posted" when the candidate is OLDER than today. Today's items are
|
||||||
|
exempt from the seen_urls gate entirely (fresh by definition).
|
||||||
|
|
||||||
|
EXPORTS:
|
||||||
|
load_posted() -> (md_urls, md_titles, seen_urls) sets
|
||||||
|
is_today(first_seen, now)
|
||||||
|
age_days(first_seen, now)
|
||||||
|
day_bucket(first_seen, now)
|
||||||
|
already_posted_fs(url,title,fs,now) -> bool (age-aware dedup)
|
||||||
|
filter_fresh(items, now) -> (today_items, older_new_items, dropped_items)
|
||||||
|
recency_weight(first_seen, now) -> float (1.0 today -> ~0 over 7d)
|
||||||
|
blend_score(item, now) -> clickability_decayed * recency_weight
|
||||||
|
Read-only against markdown + json + passed-in items. No DB writes.
|
||||||
|
"""
|
||||||
|
import os, re, json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
ORACLE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
SEEN_JSON = "/home/vpsadmin/ai-oracle-site/seen_urls.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(ts):
|
||||||
|
if not ts:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_url(u):
|
||||||
|
if not u:
|
||||||
|
return ""
|
||||||
|
return u.split("?")[0].split("#")[0].rstrip("/").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_title(t):
|
||||||
|
if not t:
|
||||||
|
return ""
|
||||||
|
t = t.lower()
|
||||||
|
t = re.sub(r"[^a-z0-9 ]", " ", t)
|
||||||
|
t = re.sub(r"\s+", " ", t).strip()
|
||||||
|
return t[:60]
|
||||||
|
|
||||||
|
|
||||||
|
def load_posted(md_dir=ORACLE, seen_json=SEEN_JSON):
|
||||||
|
"""Return (md_urls:set, md_titles:set, seen_urls:set)."""
|
||||||
|
md_urls, md_titles, seen_urls = set(), set(), set()
|
||||||
|
|
||||||
|
for fn in sorted(os.listdir(md_dir)):
|
||||||
|
if re.match(r"athena_top.*\.md$", fn):
|
||||||
|
try:
|
||||||
|
txt = open(os.path.join(md_dir, fn), encoding="utf-8", errors="replace").read()
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
for m in re.findall(r"\]\((https?://[^)\s]+)\)", txt):
|
||||||
|
nu = _norm_url(m)
|
||||||
|
if nu:
|
||||||
|
md_urls.add(nu)
|
||||||
|
for t in re.findall(r"^\|\s*\d+\s*\|\s*(.+?)\s*\|", txt, re.M):
|
||||||
|
nt = _norm_title(t)
|
||||||
|
if nt:
|
||||||
|
md_titles.add(nt)
|
||||||
|
|
||||||
|
if os.path.exists(seen_json):
|
||||||
|
try:
|
||||||
|
with open(seen_json, encoding="utf-8") as f:
|
||||||
|
for u in json.load(f):
|
||||||
|
nu = _norm_url(u)
|
||||||
|
if nu:
|
||||||
|
seen_urls.add(nu)
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return md_urls, md_titles, seen_urls
|
||||||
|
|
||||||
|
|
||||||
|
def is_today(first_seen, now=None):
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
d = _parse(first_seen)
|
||||||
|
return bool(d) and d.strftime("%Y-%m-%d") == now.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
|
||||||
|
def age_days(first_seen, now=None):
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
d = _parse(first_seen)
|
||||||
|
if not d:
|
||||||
|
return 9999.0
|
||||||
|
return max((now - d).total_seconds() / 86400.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def day_bucket(first_seen, now=None):
|
||||||
|
"""'today' | 'yesterday' | 'this-week' (<=6d) | 'older'."""
|
||||||
|
days = age_days(first_seen, now)
|
||||||
|
if days < 1:
|
||||||
|
return "today"
|
||||||
|
if days < 2:
|
||||||
|
return "yesterday"
|
||||||
|
if days <= 6:
|
||||||
|
return "this-week"
|
||||||
|
return "older"
|
||||||
|
|
||||||
|
|
||||||
|
def already_posted_fs(url, title, first_seen, now=None,
|
||||||
|
md_urls=None, md_titles=None, seen_urls=None):
|
||||||
|
"""Age-aware dedup. A candidate is 'already posted' iff:
|
||||||
|
(a) it matches the curated md-stack history, OR
|
||||||
|
(b) it is OLDER than today AND its URL is in seen_urls.json.
|
||||||
|
Today's items are NEVER flagged -- they are fresh by definition.
|
||||||
|
"""
|
||||||
|
if md_urls is None or md_titles is None or seen_urls is None:
|
||||||
|
md_urls, md_titles, seen_urls = load_posted()
|
||||||
|
if _norm_url(url) in md_urls:
|
||||||
|
return True
|
||||||
|
nt = _norm_title(title)
|
||||||
|
if nt and nt in md_titles:
|
||||||
|
return True
|
||||||
|
if is_today(first_seen, now):
|
||||||
|
return False
|
||||||
|
if _norm_url(url) in seen_urls:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def recency_weight(first_seen, now=None, half_life_days=2.0):
|
||||||
|
"""1.0 for today, decays ~halving every 2 days. The 'age' measure."""
|
||||||
|
return 0.5 ** (age_days(first_seen, now) / half_life_days)
|
||||||
|
|
||||||
|
|
||||||
|
def blend_score(item, now=None):
|
||||||
|
"""clickability_decayed * recency_weight. Today's items dominate; old sink."""
|
||||||
|
base = item.get("clickability_decayed", 0) or 0
|
||||||
|
return base * recency_weight(item.get("first_seen"), now)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_fresh(items, now=None):
|
||||||
|
"""Split into (today_items, older_new_items, dropped_items).
|
||||||
|
|
||||||
|
today_items = first_seen == today (always eligible; the week-open lead)
|
||||||
|
older_new_items= older, but never before posted (md/seen)
|
||||||
|
dropped_items = older AND already posted (the re-posts we are killing)
|
||||||
|
"""
|
||||||
|
now = now or datetime.now(timezone.utc)
|
||||||
|
md_urls, md_titles, seen_urls = load_posted()
|
||||||
|
today_items, older_new, dropped = [], [], []
|
||||||
|
for it in items:
|
||||||
|
fs = it.get("first_seen")
|
||||||
|
if is_today(fs, now):
|
||||||
|
today_items.append(it)
|
||||||
|
continue
|
||||||
|
if already_posted_fs(it.get("url"), it.get("title"), fs, now,
|
||||||
|
md_urls, md_titles, seen_urls):
|
||||||
|
dropped.append(it)
|
||||||
|
else:
|
||||||
|
older_new.append(it)
|
||||||
|
return today_items, older_new, dropped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, ORACLE)
|
||||||
|
import clickability as cb
|
||||||
|
DB = os.path.join(ORACLE, "oracle.db")
|
||||||
|
conn = sqlite3.connect(f"file:{DB}?mode=ro", uri=True)
|
||||||
|
items = cb.fetch_items(conn); conn.close()
|
||||||
|
items = cb.compute_index(items); items = cb.decay_index(items)
|
||||||
|
today_items, older_new, dropped = filter_fresh(items)
|
||||||
|
print(f"TODAY-new (week-open lead): {len(today_items)}")
|
||||||
|
print(f"OLDER-but-never-posted: {len(older_new)}")
|
||||||
|
print(f"DROPPED (already posted): {len(dropped)}")
|
||||||
|
print("\nSample dropped (the re-posts that caused the problem):")
|
||||||
|
for it in sorted(dropped, key=lambda x: -x["clickability_decayed"])[:6]:
|
||||||
|
print(f" - [{it['clickability_decayed']:.3f}] {it['title'][:66]}")
|
||||||
+902
-2008
File diff suppressed because it is too large
Load Diff
+1007
-153
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user