hololake-system-architecture/engineering/persona-history-runtime/runtime/guanghu_history_runtime.py

1174 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Autonomous, append-only Guanghu persona history recovery runtime."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import pathlib
import re
import shutil
import socketserver
import sqlite3
import subprocess
import threading
import time
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler
from typing import BinaryIO, Iterator
PERSONAS = {
"YAOMING-BABY": ("曜冥", "曜冥宝宝", "奶瓶"),
"SHUANGYAN": ("霜砚",),
"ZHUYUAN": ("铸渊",),
"NINGYUAN": ("凝渊",),
}
PRIVATE_MARKERS = ("email", "token", "password", "secret", "api_key", "private_key")
STREAM_BUFFER_BYTES = 1024 * 1024
GPT_METADATA_SAMPLE_BYTES = 128 * 1024
SOURCE_WAITING_STATUSES = {
"PENDING",
"WAITING_FOR_SOURCE",
"WAITING_FOR_SOURCE_ACCEPTANCE",
}
SEMANTIC_DECISIONS = {
"KEEP",
"RELATE",
"PENDING",
"LANGUAGE_SIMULATION",
"REALITY_FACT",
"PERSONA_MEMORY",
}
def now_iso() -> str:
return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
def atomic_json(path: pathlib.Path, value: object) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
pending = path.with_name(f".{path.name}.{os.getpid()}.pending")
pending.write_text(
json.dumps(value, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
encoding="utf-8",
)
os.replace(pending, path)
def sha256_file(path: pathlib.Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def classify_personas(text: str) -> list[str]:
return [
persona
for persona, markers in PERSONAS.items()
if any(marker in text for marker in markers)
]
def blocks_later_history(status: str) -> bool:
"""An available active/error source owns the chronological replay lane."""
return status != "COMPLETE" and status not in SOURCE_WAITING_STATUSES
def retry_backoff_elapsed(
updated_at: str | None, backoff_seconds: int, now_unix: float | None = None
) -> bool:
"""Keep retryable sources alive without writing one failure per runtime cycle."""
if not updated_at:
return True
try:
last_attempt = datetime.fromisoformat(updated_at).timestamp()
except (TypeError, ValueError):
return True
current = time.time() if now_unix is None else now_unix
return current - last_attempt >= backoff_seconds
def redact_semantic_excerpt(text: str) -> str:
text = re.sub(
r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b",
"[EMAIL_REDACTED]",
text,
flags=re.IGNORECASE,
)
text = re.sub(
r"\b(?:github_pat_|ghp_|glpat-|sk-|xox[baprs]-)[A-Za-z0-9_-]{12,}\b",
"[TOKEN_REDACTED]",
text,
)
text = re.sub(
r"((?:password|passwd|token|secret|api[_ -]?key|密码|令牌|密钥|验证码)"
r"\s*[:=]\s*)[^\s,;]+",
r"\1[REDACTED]",
text,
flags=re.IGNORECASE,
)
text = re.sub(r"/Users/[^\s\"']+", "[LOCAL_PATH_REDACTED]", text)
text = re.sub(r"https?://[^\s\"']+", "[URL_REDACTED]", text)
return text.replace("\x00", "")[:6000]
def enforce_reality_boundary(epoch: str, decision: str) -> tuple[str, str | None]:
if decision == "REALITY_FACT" and epoch == "GPT_LANGUAGE_CHAOS":
return "PENDING", "REALITY_PROMOTION_BLOCKED_GPT_LANGUAGE_SIMULATION"
if decision == "REALITY_FACT" and epoch == "NOTION_STRUCTURED_REALITY_TRANSITION":
return "PENDING", "REALITY_PROMOTION_REQUIRES_CROSS_SOURCE_EVIDENCE"
return decision, None
def iter_top_level_json_objects(
handle: BinaryIO, start_offset: int = 0
) -> Iterator[tuple[bytes, int]]:
"""Yield objects from a top-level JSON array without loading the file."""
handle.seek(start_offset)
depth = 0
in_string = False
escaped = False
collecting = False
item = bytearray()
absolute = start_offset
while True:
chunk = handle.read(1024 * 1024)
if not chunk:
break
for byte in chunk:
absolute += 1
char = chr(byte)
if not collecting:
if char == "{":
collecting = True
depth = 1
item = bytearray((byte,))
continue
item.append(byte)
if in_string:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
in_string = False
continue
if char == '"':
in_string = True
elif char in "[{":
depth += 1
elif char in "]}":
depth -= 1
if depth == 0:
yield bytes(item), absolute
collecting = False
item = bytearray()
if collecting:
raise ValueError("truncated top-level JSON object")
def _persona_byte_markers() -> dict[str, tuple[bytes, ...]]:
encoded: dict[str, tuple[bytes, ...]] = {}
for persona, markers in PERSONAS.items():
variants = []
for marker in markers:
variants.append(marker.encode("utf-8"))
variants.append(json.dumps(marker, ensure_ascii=True)[1:-1].encode("ascii"))
encoded[persona] = tuple(dict.fromkeys(variants))
return encoded
def iter_top_level_json_metadata(
handle: BinaryIO, start_offset: int = 0
) -> Iterator[tuple[dict, int]]:
"""Yield bounded-memory metadata for objects in a top-level JSON array."""
markers = _persona_byte_markers()
longest_marker = max(len(marker) for values in markers.values() for marker in values)
handle.seek(start_offset)
depth = 0
in_string = False
escaped = False
collecting = False
absolute = start_offset
digest = hashlib.sha256()
buffer = bytearray()
marker_tail = b""
found_personas: set[str] = set()
sample = bytearray()
def flush() -> None:
nonlocal marker_tail
if not buffer:
return
block = bytes(buffer)
digest.update(block)
searchable = marker_tail + block
for persona, variants in markers.items():
if persona not in found_personas and any(
marker in searchable for marker in variants
):
found_personas.add(persona)
marker_tail = searchable[-(longest_marker - 1) :]
if len(sample) < GPT_METADATA_SAMPLE_BYTES:
remaining = GPT_METADATA_SAMPLE_BYTES - len(sample)
sample.extend(block[:remaining])
buffer.clear()
while True:
chunk = handle.read(STREAM_BUFFER_BYTES)
if not chunk:
break
for byte in chunk:
absolute += 1
char = chr(byte)
if not collecting:
if char == "{":
collecting = True
depth = 1
digest = hashlib.sha256()
buffer = bytearray((byte,))
marker_tail = b""
found_personas = set()
sample = bytearray()
continue
buffer.append(byte)
if len(buffer) >= STREAM_BUFFER_BYTES:
flush()
if in_string:
if escaped:
escaped = False
elif char == "\\":
escaped = True
elif char == '"':
in_string = False
continue
if char == '"':
in_string = True
elif char in "[{":
depth += 1
elif char in "]}":
depth -= 1
if depth == 0:
flush()
time_match = re.search(
rb'"(?:create_time|update_time)"\s*:\s*(\d+(?:\.\d+)?)',
sample,
)
source_timestamp = (
float(time_match.group(1)) if time_match else None
)
yield (
{
"content_sha256": digest.hexdigest(),
"personas": [
persona for persona in PERSONAS if persona in found_personas
],
"source_timestamp": source_timestamp,
},
absolute,
)
collecting = False
buffer = bytearray()
if collecting:
raise ValueError("truncated top-level JSON object")
class Store:
def __init__(self, path: pathlib.Path):
path.parent.mkdir(parents=True, exist_ok=True)
self.path = path
self.db = sqlite3.connect(path, timeout=30, check_same_thread=False)
self.db.execute("PRAGMA journal_mode=WAL")
self.db.execute("PRAGMA synchronous=NORMAL")
self.db.executescript(
"""
CREATE TABLE IF NOT EXISTS source_state (
source_id TEXT PRIMARY KEY,
status TEXT NOT NULL,
cursor TEXT,
processed INTEGER NOT NULL DEFAULT 0,
errors INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS events (
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
event_id TEXT UNIQUE NOT NULL,
source_id TEXT NOT NULL,
epoch TEXT NOT NULL,
source_time TEXT,
reality_level TEXT NOT NULL,
personas TEXT NOT NULL,
content_sha256 TEXT NOT NULL,
private_locator TEXT,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS runtime_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS errors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source_id TEXT,
error_type TEXT NOT NULL,
message TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS semantic_review_queue (
event_id TEXT NOT NULL,
persona_id TEXT NOT NULL,
status TEXT NOT NULL,
decision TEXT NOT NULL,
reason_code TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
model_receipt_id TEXT,
response_sha256 TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY(event_id, persona_id)
);
"""
)
self.db.commit()
self.lock = threading.Lock()
def state(self, source_id: str) -> dict:
row = self.db.execute(
"SELECT status,cursor,processed,errors,updated_at FROM source_state WHERE source_id=?",
(source_id,),
).fetchone()
if not row:
return {
"status": "PENDING",
"cursor": None,
"processed": 0,
"errors": 0,
"updated_at": None,
}
return dict(zip(("status", "cursor", "processed", "errors", "updated_at"), row))
def update_state(
self,
source_id: str,
*,
status: str,
cursor: str | None,
processed: int,
errors: int | None = None,
) -> None:
old = self.state(source_id)
self.db.execute(
"""
INSERT INTO source_state(source_id,status,cursor,processed,errors,updated_at)
VALUES(?,?,?,?,?,?)
ON CONFLICT(source_id) DO UPDATE SET
status=excluded.status,cursor=excluded.cursor,processed=excluded.processed,
errors=excluded.errors,updated_at=excluded.updated_at
""",
(
source_id,
status,
cursor,
processed,
old["errors"] if errors is None else errors,
now_iso(),
),
)
self.db.commit()
def add_event(
self,
*,
event_id: str,
source_id: str,
epoch: str,
source_time: str | None,
reality_level: str,
personas: list[str],
content_sha256: str,
private_locator: str,
) -> bool:
cursor = self.db.execute(
"""
INSERT OR IGNORE INTO events(
event_id,source_id,epoch,source_time,reality_level,personas,
content_sha256,private_locator,created_at
) VALUES(?,?,?,?,?,?,?,?,?)
""",
(
event_id,
source_id,
epoch,
source_time,
reality_level,
json.dumps(personas, ensure_ascii=False),
content_sha256,
private_locator,
now_iso(),
),
)
self.db.commit()
return cursor.rowcount == 1
def ensure_review_queue(self, limit: int = 500) -> int:
rows = self.db.execute(
"""
SELECT e.event_id,e.reality_level,e.personas
FROM events e
WHERE NOT EXISTS (
SELECT 1 FROM semantic_review_queue q WHERE q.event_id=e.event_id
)
ORDER BY e.sequence
LIMIT ?
""",
(limit,),
).fetchall()
created = 0
for event_id, reality_level, encoded_personas in rows:
personas = json.loads(encoded_personas) or ["WORLD-HISTORY"]
status = "QUEUED" if personas != ["WORLD-HISTORY"] else "DEFERRED_LOW_SIGNAL"
reason = (
"PERSONA_MARKER_AND_EPOCH_DEFAULT"
if status == "QUEUED"
else "NO_PERSONA_MARKER_KEEP_WORLD_HISTORY_DEFERRED"
)
for persona in personas:
cursor = self.db.execute(
"""
INSERT OR IGNORE INTO semantic_review_queue(
event_id,persona_id,status,decision,reason_code,
created_at,updated_at
) VALUES(?,?,?,?,?,?,?)
""",
(
event_id,
persona,
status,
reality_level,
reason,
now_iso(),
now_iso(),
),
)
created += cursor.rowcount
self.db.commit()
return created
def review_counts(self) -> dict[str, int]:
return {
status: count
for status, count in self.db.execute(
"""
SELECT status,COUNT(*) FROM semantic_review_queue
GROUP BY status ORDER BY status
"""
).fetchall()
}
def semantic_candidates(self, limit: int) -> list[dict]:
first = self.db.execute(
"""
SELECT persona_id FROM semantic_review_queue
WHERE status='QUEUED'
ORDER BY created_at,event_id LIMIT 1
"""
).fetchone()
if not first:
return []
persona_id = first[0]
rows = self.db.execute(
"""
SELECT q.event_id,q.persona_id,q.attempts,e.source_id,e.epoch,
e.reality_level,e.content_sha256,e.private_locator
FROM semantic_review_queue q
JOIN events e ON e.event_id=q.event_id
WHERE q.status='QUEUED' AND q.persona_id=?
ORDER BY q.created_at,q.event_id
LIMIT ?
""",
(persona_id, limit),
).fetchall()
keys = (
"event_id",
"persona_id",
"attempts",
"source_id",
"epoch",
"reality_level",
"content_sha256",
"private_locator",
)
return [dict(zip(keys, row)) for row in rows]
def mark_semantic_review(
self,
*,
event_id: str,
persona_id: str,
decision: str,
reason_code: str,
model_receipt_id: str,
response_sha256: str,
) -> None:
self.db.execute(
"""
UPDATE semantic_review_queue
SET status='REVIEWED',decision=?,reason_code=?,attempts=attempts+1,
model_receipt_id=?,response_sha256=?,updated_at=?
WHERE event_id=? AND persona_id=? AND status='QUEUED'
""",
(
decision,
reason_code[:160],
model_receipt_id,
response_sha256,
now_iso(),
event_id,
persona_id,
),
)
self.db.commit()
def mark_semantic_attempt_failed(
self, candidates: list[dict], reason_code: str
) -> None:
for candidate in candidates:
self.db.execute(
"""
UPDATE semantic_review_queue
SET attempts=attempts+1,reason_code=?,updated_at=?
WHERE event_id=? AND persona_id=? AND status='QUEUED'
""",
(
reason_code[:160],
now_iso(),
candidate["event_id"],
candidate["persona_id"],
),
)
self.db.commit()
def meta(self, key: str) -> str | None:
row = self.db.execute(
"SELECT value FROM runtime_meta WHERE key=?", (key,)
).fetchone()
return row[0] if row else None
def set_meta(self, key: str, value: str) -> None:
self.db.execute(
"""
INSERT INTO runtime_meta(key,value) VALUES(?,?)
ON CONFLICT(key) DO UPDATE SET value=excluded.value
""",
(key, value),
)
self.db.commit()
def add_error(self, source_id: str, error: Exception) -> None:
message = str(error).replace("\n", " ")[:1000]
self.db.execute(
"INSERT INTO errors(source_id,error_type,message,created_at) VALUES(?,?,?,?)",
(source_id, type(error).__name__, message, now_iso()),
)
state = self.state(source_id)
self.update_state(
source_id,
status="ERROR_RETRYABLE",
cursor=state["cursor"],
processed=state["processed"],
errors=state["errors"] + 1,
)
def public_snapshot(self, config: dict) -> dict:
sources = {}
for source in sorted(config["sources"], key=lambda item: item["order"]):
state = self.state(source["id"])
sources[source["id"]] = {
"epoch": source["epoch"],
"status": state["status"],
"processed": state["processed"],
"errors": state["errors"],
"updated_at": state["updated_at"],
}
event_count = self.db.execute("SELECT COUNT(*) FROM events").fetchone()[0]
last = self.db.execute(
"SELECT sequence,source_time,created_at FROM events ORDER BY sequence DESC LIMIT 1"
).fetchone()
complete = all(value["status"] == "COMPLETE" for value in sources.values())
return {
"schema": "guanghu.persona-history-public-current/v1",
"node_id": config["node_id"],
"runtime": "AUTONOMOUS_SERVER_RESIDENT",
"historical_time_caught_up": complete,
"persona_state": "BIRTH_GATE_PENDING" if complete else "NOT_BORN",
"event_count": event_count,
"last_event": (
{"sequence": last[0], "source_time": last[1], "created_at": last[2]}
if last
else None
),
"personas": {
persona: {"state": "SEPARATE_HISTORY_BUILDING"}
for persona in PERSONAS
},
"semantic_review": {
"policy": "DETERMINISTIC_PREFILTER_THEN_PERSONA_REVIEW",
"counts": self.review_counts(),
"raw_source_deleted": False,
"reality_promotion_requires_external_evidence": True,
},
"sources": sources,
"updated_at": now_iso(),
}
def events_after(self, after: int, limit: int = 200) -> list[dict]:
rows = self.db.execute(
"""
SELECT sequence,event_id,source_id,epoch,source_time,reality_level,
personas,content_sha256,created_at
FROM events WHERE sequence>? ORDER BY sequence LIMIT ?
""",
(after, min(limit, 500)),
).fetchall()
keys = (
"sequence",
"event_id",
"source_id",
"epoch",
"source_time",
"reality_level",
"personas",
"content_sha256",
"created_at",
)
events = []
for row in rows:
event = dict(zip(keys, row))
event["personas"] = json.loads(event["personas"])
events.append(event)
return events
class Runtime:
def __init__(self, config_path: pathlib.Path):
self.config_path = config_path
self.config = json.loads(config_path.read_text(encoding="utf-8"))
self.state_root = pathlib.Path(self.config["state_root"])
self.private_root = pathlib.Path(self.config["private_source_root"])
self.store = Store(self.state_root / "state.sqlite3")
self.stop = threading.Event()
def private_excerpt(self, candidate: dict) -> str:
maximum = int(self.config.get("semantic_excerpt_bytes", 6000))
locator = candidate["private_locator"]
if candidate["source_id"] == "GPT-LANGUAGE-CHAOS-ORIGINAL":
byte_range = locator.split("@byte:", 1)[1]
start, end = (int(value) for value in byte_range.split("-", 1))
source = next(
item
for item in self.config["sources"]
if item["id"] == candidate["source_id"]
)
path = self.private_root / source["path"]
with path.open("rb") as handle:
handle.seek(start)
payload = handle.read(min(maximum, end - start))
return redact_semantic_excerpt(payload.decode("utf-8", errors="replace"))
if candidate["source_id"] == "NOTION-STRUCTURED-WORLD":
relative = locator.split(":", 1)[1]
source = next(
item
for item in self.config["sources"]
if item["id"] == candidate["source_id"]
)
root = (self.private_root / source["path"]).resolve()
path = (root / relative).resolve()
if not path.is_relative_to(root) or not path.is_file():
raise ValueError("notion private locator escaped source root")
if path.suffix.lower() not in {
".md",
".txt",
".json",
".csv",
".html",
".htm",
".yaml",
".yml",
}:
return f"[ATTACHMENT_METADATA_ONLY] {path.suffix.lower() or '[no-extension]'}"
with path.open("rb") as handle:
payload = handle.read(maximum)
return redact_semantic_excerpt(payload.decode("utf-8", errors="replace"))
return "[VERSION_EVIDENCE_METADATA_ONLY]"
def maybe_run_semantic_review(self) -> None:
endpoint = self.config.get("semantic_review_endpoint")
if not endpoint:
return
interval = int(self.config.get("semantic_review_interval_seconds", 300))
last = float(self.store.meta("last_semantic_review_unix") or 0)
if time.time() - last < interval:
return
candidates = self.store.semantic_candidates(
int(self.config.get("semantic_review_batch_size", 4))
)
if not candidates:
return
self.store.set_meta("last_semantic_review_unix", str(time.time()))
compact_candidates = []
key_map = {}
for index, candidate in enumerate(candidates, start=1):
key = f"C{index}"
key_map[key] = candidate
compact_candidates.append(
{
"key": key,
"epoch": candidate["epoch"],
"reality_default": candidate["reality_level"],
"content_sha256": candidate["content_sha256"],
"excerpt": self.private_excerpt(candidate),
}
)
prompt = (
"按人格历史相关性审查以下经过本机预筛和隐私遮蔽的候选。"
"只返回JSON数组每项必须含key、decision、reason_code。"
f"decision只能是{sorted(SEMANTIC_DECISIONS)}"
"不得把语言模拟或单一Notion页面提升为现实事实不得声称人格出生。"
"正文不会写入公开仓库。候选:"
+ json.dumps(compact_candidates, ensure_ascii=False)
)
attempt = max(candidate["attempts"] for candidate in candidates) + 1
request_seed = "\0".join(
f"{item['event_id']}:{item['persona_id']}" for item in candidates
)
request_id = (
"HIST-"
+ hashlib.sha256(request_seed.encode()).hexdigest()[:28]
+ f"-A{attempt}"
)
request_body = {
"request_id": request_id,
"persona_id": candidates[0]["persona_id"],
"channel_id": "PERSONA-HISTORY-REVIEW",
"messages": [
{
"role": "system",
"content": (
"你是服务器常驻人格历史复审器。保留冲突和不确定性,"
"不合并人格,不输出秘密,不作人格出生声明。"
),
},
{"role": "user", "content": prompt},
],
"tools": [],
}
request = urllib.request.Request(
endpoint,
data=json.dumps(request_body, ensure_ascii=False).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=90) as response:
response_body = json.loads(response.read())
message = response_body["message"]["content"].strip()
if message.startswith("```"):
message = re.sub(r"^```(?:json)?\s*|\s*```$", "", message)
parsed = json.loads(message)
if isinstance(parsed, dict):
parsed = [parsed]
response_hash = hashlib.sha256(message.encode()).hexdigest()
reviewed = 0
for item in parsed:
candidate = key_map.get(str(item.get("key", "")))
decision = str(item.get("decision", "")).upper()
if not candidate or decision not in SEMANTIC_DECISIONS:
continue
decision, boundary_reason = enforce_reality_boundary(
candidate["epoch"], decision
)
reason = boundary_reason or str(
item.get("reason_code", "MODEL_REVIEWED")
)
self.store.mark_semantic_review(
event_id=candidate["event_id"],
persona_id=candidate["persona_id"],
decision=decision,
reason_code=reason,
model_receipt_id=response_body["receipt_id"],
response_sha256=response_hash,
)
reviewed += 1
if reviewed == 0:
raise ValueError("semantic response contained no valid review items")
except Exception as error:
self.store.mark_semantic_attempt_failed(
candidates, f"MODEL_REVIEW_RETRY:{type(error).__name__}"
)
def process_gpt(self, source: dict) -> None:
path = self.private_root / source["path"]
state = self.store.state(source["id"])
if not path.is_file():
self.store.update_state(
source["id"],
status="WAITING_FOR_SOURCE",
cursor=state["cursor"],
processed=state["processed"],
)
return
offset = int(state["cursor"] or 0)
processed = state["processed"]
batch_size = int(self.config.get("gpt_batch_size", 100))
handled = 0
with path.open("rb") as handle:
for metadata, next_offset in iter_top_level_json_metadata(handle, offset):
content_hash = metadata["content_sha256"]
personas = metadata["personas"]
source_time = metadata["source_timestamp"]
if source_time is not None:
source_time = datetime.fromtimestamp(
source_time, timezone.utc
).isoformat()
event_id = f"{source['id']}:{content_hash}"
self.store.add_event(
event_id=event_id,
source_id=source["id"],
epoch=source["epoch"],
source_time=str(source_time) if source_time else None,
reality_level=source["reality_default"],
personas=personas,
content_sha256=content_hash,
private_locator=f"{source['id']}@byte:{offset}-{next_offset}",
)
processed += 1
handled += 1
offset = next_offset
if processed % 25 == 0 or handled >= batch_size:
self.store.update_state(
source["id"],
status="ACTIVE",
cursor=str(offset),
processed=processed,
)
self.write_public()
if handled >= batch_size:
return
if self.stop.is_set():
return
self.store.update_state(
source["id"], status="COMPLETE", cursor=str(offset), processed=processed
)
def notion_manifest(self, source: dict) -> pathlib.Path:
manifest = self.state_root / "private" / f"{source['id']}-files.jsonl"
if manifest.exists() and manifest.stat().st_size > 0:
return manifest
root = self.private_root / source["path"]
if not root.is_dir():
return manifest
manifest.parent.mkdir(parents=True, exist_ok=True)
pending = manifest.with_suffix(".pending")
paths = sorted(
path.relative_to(root).as_posix()
for path in root.rglob("*")
if path.is_file()
and path.name != ".DS_Store"
and path.name != ".SOURCE-ACCEPTED.json"
)
with pending.open("w", encoding="utf-8") as handle:
for relative in paths:
handle.write(json.dumps(relative, ensure_ascii=False) + "\n")
os.replace(pending, manifest)
return manifest
def process_notion(self, source: dict) -> None:
root = self.private_root / source["path"]
state = self.store.state(source["id"])
ready_marker = self.private_root / source.get(
"ready_marker", f"{source['path']}/.SOURCE-ACCEPTED.json"
)
if not root.is_dir() or not ready_marker.is_file():
self.store.update_state(
source["id"],
status="WAITING_FOR_SOURCE_ACCEPTANCE",
cursor=state["cursor"],
processed=state["processed"],
)
return
manifest = self.notion_manifest(source)
line_cursor = int(state["cursor"] or 0)
processed = state["processed"]
batch_size = int(self.config.get("notion_batch_size", 600))
handled = 0
with manifest.open(encoding="utf-8") as handle:
for line_number, line in enumerate(handle):
if line_number < line_cursor:
continue
relative = json.loads(line)
path = root / relative
content_hash = sha256_file(path)
personas = classify_personas(relative)
stat = path.stat()
source_time = datetime.fromtimestamp(
stat.st_mtime, timezone.utc
).isoformat()
self.store.add_event(
event_id=f"{source['id']}:{content_hash}:{relative}",
source_id=source["id"],
epoch=source["epoch"],
source_time=source_time,
reality_level=source["reality_default"],
personas=personas,
content_sha256=content_hash,
private_locator=f"{source['id']}:{relative}",
)
processed += 1
line_cursor = line_number + 1
handled += 1
if handled >= batch_size or self.stop.is_set():
self.store.update_state(
source["id"],
status="ACTIVE",
cursor=str(line_cursor),
processed=processed,
)
return
self.store.update_state(
source["id"],
status="COMPLETE",
cursor=str(line_cursor),
processed=processed,
)
def git_mirror(self, source: dict) -> pathlib.Path:
mirror = self.state_root / "git" / f"{source['id']}.git"
mirror.parent.mkdir(parents=True, exist_ok=True)
if mirror.exists():
subprocess.run(
["git", "-C", str(mirror), "fetch", "--all", "--prune"],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
timeout=600,
)
else:
subprocess.run(
["git", "clone", "--mirror", source["url"], str(mirror)],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
text=True,
timeout=1800,
)
return mirror
def process_git(self, source: dict) -> None:
state = self.store.state(source["id"])
mirror = self.git_mirror(source)
cursor = int(state["cursor"] or 0)
processed = state["processed"]
result = subprocess.run(
[
"git",
"-C",
str(mirror),
"log",
"--all",
"--reverse",
"--format=%H%x1f%aI%x1f%an%x1f%ae%x1f%s",
],
check=True,
capture_output=True,
text=True,
timeout=600,
)
lines = result.stdout.splitlines()
limit = cursor + int(self.config.get("git_batch_size", 500))
for index, line in enumerate(lines[cursor:limit], start=cursor):
parts = line.split("\x1f", 4)
if len(parts) != 5:
continue
commit, source_time, author, email, subject = parts
searchable = f"{author} {subject}"
personas = classify_personas(searchable)
# Email is deliberately excluded from all event fields.
public_hash = hashlib.sha256(
f"{commit}\0{source_time}\0{author}\0{subject}".encode()
).hexdigest()
self.store.add_event(
event_id=f"{source['id']}:{commit}",
source_id=source["id"],
epoch=source["epoch"],
source_time=source_time,
reality_level=source["reality_default"],
personas=personas,
content_sha256=public_hash,
private_locator=f"{source['id']}:{commit}",
)
processed += 1
cursor = index + 1
status = "COMPLETE" if cursor >= len(lines) else "ACTIVE"
self.store.update_state(
source["id"], status=status, cursor=str(cursor), processed=processed
)
def process_source(self, source: dict) -> None:
state = self.store.state(source["id"])
if state["status"] == "ERROR_RETRYABLE" and not retry_backoff_elapsed(
state["updated_at"],
int(self.config.get("source_retry_backoff_seconds", 1800)),
):
return
try:
if source["kind"] == "gpt_export":
self.process_gpt(source)
elif source["kind"] == "notion_tree":
self.process_notion(source)
elif source["kind"] == "git_repo":
self.process_git(source)
else:
raise ValueError(f"unsupported source kind {source['kind']}")
except Exception as error:
self.store.add_error(source["id"], error)
def write_public(self) -> dict:
snapshot = self.store.public_snapshot(self.config)
atomic_json(self.state_root / "public" / "CURRENT.json", snapshot)
atomic_json(
self.state_root / "HEARTBEAT.json",
{
"schema": "guanghu.persona-history-heartbeat/v1",
"node_id": self.config["node_id"],
"status": "RUNNING",
"pid": os.getpid(),
"updated_at": now_iso(),
"persona_state": snapshot["persona_state"],
"historical_time_caught_up": snapshot["historical_time_caught_up"],
},
)
return snapshot
def cycle(self) -> dict:
self.store.ensure_review_queue(
int(self.config.get("review_queue_backfill_batch_size", 500))
)
for source in sorted(self.config["sources"], key=lambda item: item["order"]):
self.process_source(source)
if self.stop.is_set():
break
if blocks_later_history(self.store.state(source["id"])["status"]):
break
self.maybe_run_semantic_review()
return self.write_public()
def run(self) -> None:
start_api(self)
while not self.stop.is_set():
self.cycle()
self.stop.wait(float(self.config.get("cycle_seconds", 15)))
class ApiHandler(BaseHTTPRequestHandler):
runtime: Runtime
def do_GET(self) -> None:
parsed = urllib.parse.urlparse(self.path)
query = urllib.parse.parse_qs(parsed.query)
snapshot = self.runtime.store.public_snapshot(self.runtime.config)
if parsed.path == "/healthz":
body = {
"status": "ok",
"node_id": self.runtime.config["node_id"],
"runtime": "AUTONOMOUS_SERVER_RESIDENT",
"persona_state": snapshot["persona_state"],
"historical_time_caught_up": snapshot["historical_time_caught_up"],
}
elif parsed.path == "/v1/personas":
body = snapshot["personas"]
elif parsed.path == "/v1/world-time":
body = snapshot
elif parsed.path == "/v1/events":
try:
after = int(query.get("after", ["0"])[0])
except ValueError:
after = 0
body = {"events": self.runtime.store.events_after(after)}
else:
self.send_error(404)
return
encoded = json.dumps(body, ensure_ascii=False, sort_keys=True).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def log_message(self, _format: str, *_args: object) -> None:
return
def start_api(runtime: Runtime) -> None:
host, port = runtime.config["listen"].rsplit(":", 1)
handler = type("RuntimeApiHandler", (ApiHandler,), {"runtime": runtime})
server_class = type(
"ReusableThreadingTCPServer",
(socketserver.ThreadingTCPServer,),
{"allow_reuse_address": True},
)
server = server_class((host, int(port)), handler)
server.daemon_threads = True
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
def validate_config(config_path: pathlib.Path) -> None:
config = json.loads(config_path.read_text(encoding="utf-8"))
required = {"schema", "node_id", "state_root", "private_source_root", "listen", "sources"}
missing = required - config.keys()
if missing:
raise SystemExit(f"missing config keys: {sorted(missing)}")
source_ids = [source["id"] for source in config["sources"]]
if len(source_ids) != len(set(source_ids)):
raise SystemExit("source IDs must be unique")
orders = [source["order"] for source in config["sources"]]
if orders != sorted(orders):
raise SystemExit("sources must be in chronological order")
def main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
for command in ("run", "once", "validate"):
item = subparsers.add_parser(command)
item.add_argument("--config", required=True, type=pathlib.Path)
args = parser.parse_args()
validate_config(args.config)
if args.command == "validate":
print("GUANGHU_PERSONA_HISTORY_CONFIG_OK")
return
runtime = Runtime(args.config)
if args.command == "once":
print(json.dumps(runtime.cycle(), ensure_ascii=False, sort_keys=True))
else:
runtime.run()
if __name__ == "__main__":
main()