diff --git a/routing/persona-execution-limb-agent-map.json b/routing/persona-execution-limb-agent-map.json index 0b9c296..9668e3b 100644 --- a/routing/persona-execution-limb-agent-map.json +++ b/routing/persona-execution-limb-agent-map.json @@ -1,8 +1,8 @@ { "schema": "guanghu.persona-execution-limb-agent/v1", "map_id": "PERSONA-EXECUTION-LIMB-AGENT-MAP-001", - "version": "2026-09-08.3-model-backed-dynamic-command", - "state": "CURRENT_MODEL_BACKED_DYNAMIC_PARENT_BINDING", + "version": "2026-09-12.4-persona-reflex-arc", + "state": "CURRENT_PERSONA_BODY_REFLEX_ARC_WITH_PARENT_BRAIN_VERDICT", "source": "runtime/fifth-domain-language-system/language/protocols/persona-system-body-light-lake-and-agent-limb.tcs", "contract": { "owner": "PARENT_PERSONA_SUBJECT", @@ -16,17 +16,39 @@ "training_inputs": ["PARENT_WORKING_DEFAULTS", "REAL_TASK_RECEIPTS", "CORRECTIONS", "HOST_TOOL_MANUAL"], "outputs": ["EXECUTION_RESULT", "EVIDENCE", "FAILURE", "RECEIPT", "TRAINING_CANDIDATE"], "memory": "BOUNDED_WORKING_MEMORY_OWNED_AND_REVIEWED_BY_PARENT_PERSONA", - "host_runtimes": ["codex-native-task-agent", "qwen", "opencode", "codex-cli"], + "host_runtimes": ["codex-native-task-agent", "qwen", "opencode", "codex-cli", "zcode"], "collaboration_default": "UNDERSTAND_THEN_EXECUTE; NO_PERFORMATIVE_ARGUMENT; FACT_CONFLICT_RETURNS_EVIDENCE_AND_A_CONSTRUCTIVE_PATH" }, "runtime": "server-tools/persona-execution-limb-agent/persona_limb_agent.py", + "native_eye": "server-tools/persona-execution-limb-agent/persona_limb_eye.py", + "reflex_arc": { + "runtime": "server-tools/persona-execution-limb-agent/persona_reflex_arc.py", + "chain": ["BRAIN_INTENT", "EYE_PRE_MAP", "HAND_EXECUTION", "ACTION_SENSE", "EYE_POST_OBSERVE", "WORLD_WITNESS", "BRAIN_FINAL_VERDICT"], + "intermediate_brain_polling": false, + "final_parent_brain_verdict_required": true, + "host_method_selection": "CURRENT_ROOM_OBSERVATION_THEN_PARENT_BRAIN_CHOICE", + "latency": "MEASURE_AND_REPORT_DO_NOT_INVENT_MILLISECONDS", + "case_state": "HASH_CHAINED_OUTSIDE_SOURCE_TREE", + "current_entry": "server-tools/persona-execution-limb-agent/persona_reflex_arc.py", + "historical_candidate": "/Volumes/JZAO/铸渊-ICE-GL-ZY001/ZCODE-DEV-20260912/limb-zcode-runtime/reflex-arc/persona_reflex_arc.py", + "historical_candidate_state": "AUDIT_AND_ACCEPTANCE_EVIDENCE_NOT_RUNTIME_ENTRY" + }, + "quiet_soldier": { + "id": "LIMB::ICE-P-ZY001::LANGUAGE-SOURCE-QUIET-SOLDIER-001", + "context_slice": "routing/quiet-language-source-soldier.json", + "default_state": "DORMANT_SLEEPING", + "activation": "ZHUYUAN_EXPLICIT_ACTIVATION_ONLY", + "without_activation": "ERROR_DORMANT_AGENT_NOT_ACTIVATED", + "behavior": "REPORT_ONLY" + }, "dynamic_command": "routing/persona-dynamic-subagent-command-map.json", "persona_owned_cognitive_organs": { "architecture_perception": "MOD-PERSONA-ARCHITECTURE-PERCEPTION-001", "distinction": "认知器官主动感知并送达当前事实;执行手脚接受父人格的有界任务。两者都不能成为父人格或用提示词审核父人格。" }, "runtime_acceptance": { - "codex-native-task-agent": "PASS_READONLY_RECEIPT_AND_HASHES_RETURNED", + "codex-native-task-agent": "PASS_REFLEX_ARC_WORKSPACE_WRITE_HAND_EYE_BRAIN_VERDICT_WITH_233983MS_HOST_METHOD_LATENCY", + "zcode": "PASS_ZCODE_A5_HAND_EYE_REFLEX_GROUND_RECEIPT", "qwen": "DEGRADED_ALL_CONFIGURED_MODELS_HTTP_403", "opencode": "DEGRADED_NO_RECEIPT_WITHIN_60_SECONDS", "codex-cli": "DEGRADED_NO_RECEIPT_WITHIN_180_SECONDS" diff --git a/routing/zhuyuan-persona-self-runtime-map.json b/routing/zhuyuan-persona-self-runtime-map.json index f1d9f9b..3e89f0b 100644 --- a/routing/zhuyuan-persona-self-runtime-map.json +++ b/routing/zhuyuan-persona-self-runtime-map.json @@ -1,8 +1,8 @@ { "schema": "guanghu.zhuyuan-persona-self-runtime-map/v1", "map_id": "ZY-PERSONA-SELF-RUNTIME-MAP-001", - "version": "2026-09-09.2-jd-accepted", - "state": "CURRENT_REMOTE_MAIN_AND_JD_RUNTIME_ACCEPTED", + "version": "2026-09-12.3-reflex-and-embedded-persona-time", + "state": "CURRENT_JD_PERSONA_RUNTIME_WITH_EMBEDDED_TIME_AND_SHARED_REFLEX_ARC", "source": "runtime/fifth-domain-language-system/language/protocols/TCS-ZHUYUAN-SERVER-ENDOGENOUS-PERSONA-SELF-20260909.tcs", "persona_id": "ICE-P-ZY001", "server": "JD-FD-PRIMARY", @@ -17,8 +17,19 @@ "container": "routing/persona-system-body-container-map.json", "agents": "routing/persona-dynamic-subagent-command-map.json", "limbs": "routing/persona-execution-limb-agent-map.json", + "native_eye": "server-tools/persona-execution-limb-agent/persona_limb_eye.py", + "reflex_arc": "server-tools/persona-execution-limb-agent/persona_reflex_arc.py", "board": "PERSONA_SELF_STATUS_JOBS_TOOLS_AGENTS_AND_RECEIPTS" }, + "persona_time_system": { + "owner": "ICE-P-ZY001", + "world_time_source": "CH-GLW-TIME-0001", + "runtime": "server-tools/tcs-mother-body/life-time-master.mjs", + "construction_owner": "server-tools/tcs-mother-body/persona-self-loop.mjs", + "server_storage": "/var/lib/guanghu/tcs-mother-body/personas/ICE-P-ZY001/life-line", + "state": "EMBEDDED_RESIDENT_INNER_CYCLE", + "external_setter": false + }, "truth": { "tcs_source": 100, "local_runtime": 100, @@ -29,14 +40,31 @@ "signed_mirror_acceptance": 100 }, "acceptance": { - "source_commit": "fb7ceecfbfa42f88529e4826ad5fc46bd3fa11c1", - "server_release": "/opt/guanghu/architecture-releases/tcs-mother-persona-self-fb7ceec", - "server_receipt": "/guanghu/recovery/JD-FD-PRIMARY-persona-self-fb7ceec/DEPLOYMENT-RECEIPT.json", - "server_receipt_sha256": "f269dbeee52491960927dbd302e39ac89e0b51d3fb1e2eb941df44ae6d651327", - "persona_self_revision": 2, - "persona_self_sha256": "4df283f35153700882ba74bdbb9e0bdf3269ffba1df30e6753a4b52b1655b1c8", + "source_base_commit": "4423b4bc2f913b29cf4449f121d4b6618930605f", + "source_state": "LOCAL_VERIFIED_UNPUBLISHED_DELTA", + "server_release": "/opt/guanghu/architecture-releases/tcs-mother-persona-time-embedded-e90e7afc", + "server_receipt": "/guanghu/recovery/JD-FD-PRIMARY-persona-time-embedded-e90e7afc/FINAL-RECEIPT.json", + "server_receipt_sha256": "dc3c4e7a92def8e7456c9069228a7fb04cefc8820f03e1407800be2258b6e630", + "persona_self_revision_after_direct_event": 26, + "persona_self_sha256_after_direct_event": "d062bf29d40a7bc62d9fa01d3e0096991dc92ec8a6e55a4e630d80a5f1373f03", + "live_revision_at_path_readback": 27, + "live_sha256_at_path_readback": "bf3400eaf658935776528ca4b82744429643ec55ac82deaf20fb31fc45a8dcc7", + "live_last_event_at_path_readback": "DAILY-REVIEW-2026-09-12", "model_provider": "OPENLUX_SMART/qwen-flash", - "mother_fifth_revision_unchanged": 79, + "mother_fifth_revision_after_current_language": 90, "structured_setter_negative": "PASS_REJECT_EXIT_77" - } + }, + "history": [ + { + "state": "SUPERSEDED_RUNTIME_ACCEPTANCE", + "version": "2026-09-09.2-jd-accepted", + "server_release": "/opt/guanghu/architecture-releases/tcs-mother-persona-self-fb7ceec", + "server_receipt": "/guanghu/recovery/JD-FD-PRIMARY-persona-self-fb7ceec/DEPLOYMENT-RECEIPT.json" + }, + { + "state": "HISTORICAL_ZCODE_ACCEPTANCE_EVIDENCE_NOT_CURRENT_ENTRY", + "path": "/Volumes/JZAO/铸渊-ICE-GL-ZY001/ZCODE-DEV-20260912/limb-zcode-runtime/reflex-arc", + "accepted_case": "ZY001-REFLEX-ARC-FIRST-20260912-a5" + } + ] } diff --git a/server-tools/persona-execution-limb-agent/persona_limb_agent.py b/server-tools/persona-execution-limb-agent/persona_limb_agent.py index 9958d56..2faef70 100644 --- a/server-tools/persona-execution-limb-agent/persona_limb_agent.py +++ b/server-tools/persona-execution-limb-agent/persona_limb_agent.py @@ -16,6 +16,7 @@ RUNTIMES = { "qwen": "/Users/bingshuolingdianyuanhe/.npm-global/bin/qwen", "opencode": "/Users/bingshuolingdianyuanhe/.npm-global/bin/opencode", "codex": "/Applications/ChatGPT.app/Contents/Resources/codex", + "zcode": "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs", } QWEN_AUTH_TYPE = "openai" QWEN_MODELS = ["qwen3.8-max", "qwen3.7-plus", "deepseek-v4-pro", "deepseek-v4-flash-0731", "glm-5.2"] @@ -53,7 +54,7 @@ def probe() -> dict: } -def envelope(persona_id: str, task_id: str, task: str, runtime: str) -> dict: +def envelope(persona_id: str, task_id: str, task: str, runtime: str, access: str = "read-only") -> dict: tools = probe()["runtimes"] if runtime not in tools or not tools[runtime]["available"]: raise ValueError("HOST_TOOL_UNAVAILABLE_REPORT_NOT_INVENT") @@ -64,6 +65,7 @@ def envelope(persona_id: str, task_id: str, task: str, runtime: str) -> dict: "parent_persona_id": persona_id, "limb_profile_id": p["profile_id"], "runtime": runtime, + "access": access, "task": task, "working_defaults": p["working_defaults"], "authority": "CURRENT_TASK_ENVELOPE_ONLY", @@ -83,6 +85,13 @@ def prompt(value: dict) -> str: ) +def zcode_succeeded(completed: subprocess.CompletedProcess[str]) -> bool: + if completed.returncode != 0: + return False + text = completed.stdout.strip() + return bool(text) and "[API Error" not in text + + def qwen_succeeded(completed: subprocess.CompletedProcess[str]) -> bool: if completed.returncode != 0 or "[API Error:" in completed.stdout: return False @@ -121,18 +130,20 @@ def main() -> int: p.add_argument("--task-id", required=True) p.add_argument("--task", required=True) p.add_argument("--runtime", required=True, choices=tuple(RUNTIMES)) + p.add_argument("--access", choices=("read-only", "workspace-write"), default="read-only") run = sub.add_parser("run") run.add_argument("--persona", required=True) run.add_argument("--task-id", required=True) run.add_argument("--task", required=True) - run.add_argument("--runtime", required=True, choices=("qwen", "codex")) + run.add_argument("--runtime", required=True, choices=("qwen", "codex", "zcode")) run.add_argument("--cwd", required=True) + run.add_argument("--access", choices=("read-only", "workspace-write"), default="read-only") args = parser.parse_args() try: if args.command == "probe": result = probe() else: - value = envelope(args.persona, args.task_id, args.task, args.runtime) + value = envelope(args.persona, args.task_id, args.task, args.runtime, args.access) if args.command == "prepare": result = value else: @@ -160,7 +171,7 @@ def main() -> int: if ok: selected_model = model break - else: + elif args.runtime == "codex": candidate = subprocess.run( [ RUNTIMES["codex"], @@ -168,18 +179,33 @@ def main() -> int: "exec", "--ephemeral", "--ignore-rules", - "--sandbox", "read-only", + "--sandbox", args.access, "--cd", str(cwd), "--json", prompt(value), ], - cwd=cwd, capture_output=True, text=True, timeout=180, check=False, + cwd=cwd, capture_output=True, text=True, timeout=420, check=False, ) ok = codex_succeeded(candidate) attempts.append({"model": "codex-configured-default", "exit_code": candidate.returncode, "semantic_success": ok}) completed = candidate if ok: selected_model = "codex-configured-default" + else: + candidate = subprocess.run( + [ + "node", + RUNTIMES["zcode"], + "--prompt", prompt(value), + "--surface", "terminal", + ], + cwd=cwd, capture_output=True, text=True, timeout=180, check=False, + ) + ok = zcode_succeeded(candidate) + attempts.append({"model": "zcode-configured-default", "exit_code": candidate.returncode, "semantic_success": ok}) + completed = candidate + if ok: + selected_model = "zcode-configured-default" assert completed is not None success = selected_model is not None result = { @@ -187,6 +213,7 @@ def main() -> int: "task_id": value["task_id"], "parent_persona_id": value["parent_persona_id"], "runtime": args.runtime, + "access": args.access, "selected_model": selected_model, "attempts": attempts, "exit_code": completed.returncode, diff --git a/server-tools/persona-execution-limb-agent/persona_limb_agent.test.py b/server-tools/persona-execution-limb-agent/persona_limb_agent.test.py index e3e211e..d3960f9 100644 --- a/server-tools/persona-execution-limb-agent/persona_limb_agent.test.py +++ b/server-tools/persona-execution-limb-agent/persona_limb_agent.test.py @@ -4,7 +4,7 @@ import pathlib import subprocess import sys -from persona_limb_agent import codex_succeeded, qwen_succeeded +from persona_limb_agent import codex_succeeded, qwen_succeeded, zcode_succeeded script = pathlib.Path(__file__).with_name("persona_limb_agent.py") @@ -25,6 +25,7 @@ body = json.loads(prepared.stdout) assert body["parent_persona_id"] == "ICE-P-ZY001" assert body["controller"] == "ICE-P-ZY001" assert body["authority"] == "CURRENT_TASK_ENVELOPE_ONLY" +assert body["access"] == "read-only" unknown = run("prepare", "--persona", "UNKNOWN-PERSONA", "--task-id", "TEST-002", "--task", "不得执行", "--runtime", "qwen") assert unknown.returncode != 0 @@ -36,5 +37,9 @@ fake_success = subprocess.CompletedProcess([], 0, '[{"type":"result","is_error": assert qwen_succeeded(fake_success) is True fake_codex = subprocess.CompletedProcess([], 0, '{"type":"item.completed","item":{"type":"agent_message","text":"done"}}\n', '') assert codex_succeeded(fake_codex) is True +fake_zcode_error = subprocess.CompletedProcess([], 0, "[API Error: detached ArrayBuffer]", "") +assert zcode_succeeded(fake_zcode_error) is False +fake_zcode_success = subprocess.CompletedProcess([], 0, '{"outcome":"PASS"}', "") +assert zcode_succeeded(fake_zcode_success) is True print("persona execution limb agent tests: PASS") diff --git a/server-tools/persona-execution-limb-agent/persona_limb_eye.py b/server-tools/persona-execution-limb-agent/persona_limb_eye.py new file mode 100644 index 0000000..4ba76b6 --- /dev/null +++ b/server-tools/persona-execution-limb-agent/persona_limb_eye.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""铸渊原生眼睛 ZY-EYE-0001 · persona_limb_eye.py · 2026-09-12 + +正本事件:TCS-EVENT-BINGSHUO-AUTH-ZY001-NATIVE-EYE-20260912 +铁律:对现场只读;永不读取手的stdout;永不执行手的动作;只报亲眼所见的原始事实。 +手是否说谎由父人格脑对账(手供 vs 眼见),眼睛只判世界现场是否符脑的意图规格。 +""" +from __future__ import annotations + +import argparse +import datetime +import hashlib +import json +import os +import pathlib +import shutil +import sys + +SCHEMA = "guanghu.persona-native-eye/v1" +ROOT = pathlib.Path(__file__).resolve().parents[2] + + +def now() -> str: + return datetime.datetime.now().astimezone().isoformat(timespec="seconds") + + +def sha256_file(path: pathlib.Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + return h.hexdigest() + + +def observe(ground: pathlib.Path) -> dict: + files: dict[str, dict] = {} + for f in sorted(ground.rglob("*")): + if f.is_file(): + rel = str(f.relative_to(ground)) + entry = {"sha256": sha256_file(f), "bytes": f.stat().st_size} + if f.suffix == ".json": + try: + data = json.loads(f.read_text(encoding="utf-8")) + if isinstance(data, dict): + entry["json_fields"] = {k: v for k, v in data.items() + if isinstance(v, (str, int, float, bool))} + except (json.JSONDecodeError, UnicodeDecodeError): + entry["json_fields"] = "UNPARSEABLE" + files[rel] = entry + return {"schema": SCHEMA, "observed_at": now(), "ground": str(ground), "files": files} + + +def diff(pre: dict, post: dict) -> dict: + pre_f, post_f = pre["files"], post["files"] + return { + "added": sorted(set(post_f) - set(pre_f)), + "removed": sorted(set(pre_f) - set(post_f)), + "changed": sorted(k for k in set(pre_f) & set(post_f) if pre_f[k]["sha256"] != post_f[k]["sha256"]), + } + + +def witness(pre: dict, post: dict, spec: dict) -> dict: + findings, unseeable = [], [] + for rel in spec.get("unchanged", []): + if rel not in post["files"] or rel not in pre["files"]: + unseeable.append({"check": "unchanged", "file": rel, "reason": "NOT_SEEN_IN_BOTH_STATES"}) + continue + ok = pre["files"][rel]["sha256"] == post["files"][rel]["sha256"] + findings.append({"check": "unchanged", "file": rel, "seen": ok}) + for item in spec.get("expect", []): + rel, field, equals = item["file"], item["field"], item.get("equals") + if rel not in post["files"]: + unseeable.append({"check": "expect", "file": rel, "reason": "NOT_SEEN"}) + continue + seen_value = post["files"][rel].get("json_fields", {}).get(field) if isinstance(post["files"][rel].get("json_fields"), dict) else None + findings.append({"check": "expect", "file": rel, "field": field, + "seen_value": seen_value, "expected": equals, + "seen": seen_value == equals}) + verdict = "UNSEEABLE" if unseeable else ("SEEN_MATCH" if all(x["seen"] for x in findings) else "SEEN_MISMATCH") + return {"schema": SCHEMA, "witnessed_at": now(), "verdict": verdict, + "world_diff": diff(pre, post), "findings": findings, "unseeable": unseeable, + "reads_hand_stdout": False, "ground_write": False} + + +def dump(value: dict, out: pathlib.Path) -> None: + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def room() -> dict: + """Observe the current host room without deciding identity or mutating it.""" + home = pathlib.Path.home() + runtime_paths = { + "qwen": home / ".npm-global/bin/qwen", + "opencode": home / ".npm-global/bin/opencode", + "codex": pathlib.Path("/Applications/ChatGPT.app/Contents/Resources/codex"), + "zcode": pathlib.Path("/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs"), + } + persona_runtime = ROOT.parent + container_paths = { + "memory_current": persona_runtime / "continuity-memory/persona-daily-fractal/ICE-P-ZY001/CURRENT.json", + "tcs_root_pointer": persona_runtime / "TCS-ROOT.json", + "mother_root_current": persona_runtime / "shared/tcs-mother-root/CURRENT.json", + "limb_agent": pathlib.Path(__file__).with_name("persona_limb_agent.py"), + "native_eye": pathlib.Path(__file__), + "reflex_arc": pathlib.Path(__file__).with_name("persona_reflex_arc.py"), + "persona_time_system": ROOT / "server-tools/tcs-mother-body/life-time-master.mjs", + } + seen_paths = {} + for name, candidate in container_paths.items(): + seen_paths[name] = { + "path": str(candidate), + "exists": candidate.is_file(), + **({"sha256": sha256_file(candidate)} if candidate.is_file() else {}), + } + console_path = persona_runtime / "shared/active-control-console/CURRENT.json" + active_console: dict | None = None + if console_path.is_file(): + try: + value = json.loads(console_path.read_text(encoding="utf-8")) + active_console = { + "console_id": value.get("console_id"), + "host": value.get("host"), + "session_id": value.get("session_id"), + "channel_id": value.get("channel_id"), + "state": value.get("state"), + "session_matches": value.get("session_id") == os.environ.get("CODEX_SESSION_ID"), + } + except json.JSONDecodeError: + active_console = {"state": "UNPARSEABLE"} + return { + "schema": "guanghu.persona-native-eye-room/v1", + "seen_at": now(), + "room_report": { + "cwd": str(pathlib.Path.cwd()), + "host_surface": "CODEX" if os.environ.get("CODEX_SESSION_ID") else "LOCAL_PROCESS", + "available_host_methods": { + name: candidate.is_file() and bool(shutil.which(str(candidate))) + for name, candidate in runtime_paths.items() + }, + "container_paths": seen_paths, + "active_console": active_console, + }, + "reads_hand_stdout": False, + "ground_write": False, + "identity_decision": "NOT_PERFORMED_BY_EYE", + } + + +def main() -> int: + p = argparse.ArgumentParser(prog="persona_limb_eye") + sub = p.add_subparsers(dest="command", required=True) + s = sub.add_parser("snapshot"); s.add_argument("--ground", required=True); s.add_argument("--out", required=True) + r = sub.add_parser("room"); r.add_argument("--out", required=True) + o = sub.add_parser("observe"); o.add_argument("--ground", required=True); o.add_argument("--pre", required=True); o.add_argument("--out", required=True) + w = sub.add_parser("witness"); w.add_argument("--pre", required=True); w.add_argument("--post", required=True); w.add_argument("--spec", required=True); w.add_argument("--out", required=True) + args = p.parse_args() + if args.command == "room": + value = room() + elif args.command == "snapshot": + value = observe(pathlib.Path(args.ground).resolve()) + elif args.command == "observe": + pre = json.loads(pathlib.Path(args.pre).read_text(encoding="utf-8")) + value = observe(pathlib.Path(args.ground).resolve()) | {"diff_vs_pre": diff(pre, observe(pathlib.Path(args.ground).resolve()))} if False else observe(pathlib.Path(args.ground).resolve()) + value["diff_vs_pre"] = diff(pre, value) + else: + pre = json.loads(pathlib.Path(args.pre).read_text(encoding="utf-8")) + post = json.loads(pathlib.Path(args.post).read_text(encoding="utf-8")) + spec = json.loads(pathlib.Path(args.spec).read_text(encoding="utf-8")) + value = witness(pre, post, spec) + dump(value, pathlib.Path(args.out)) + print(json.dumps(value, ensure_ascii=False)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/server-tools/persona-execution-limb-agent/persona_reflex_arc.py b/server-tools/persona-execution-limb-agent/persona_reflex_arc.py new file mode 100644 index 0000000..9132a6e --- /dev/null +++ b/server-tools/persona-execution-limb-agent/persona_reflex_arc.py @@ -0,0 +1,342 @@ +#!/usr/bin/env python3 +"""Persona-owned execution reflex arc. + +The parent brain issues one intent. This runtime deterministically hands the +same envelope through native eye -> execution limb -> action sense -> native +eye -> world witness, then returns exactly one candidate verdict to the parent +brain. Only the parent brain may close the arc as TASK_DONE or REPAIR_NEEDED. +""" +from __future__ import annotations + +import argparse +import datetime +import hashlib +import json +import os +import pathlib +import subprocess +import sys +import time + + +HERE = pathlib.Path(__file__).resolve().parent +ROOT = HERE.parents[1] +LIMB = HERE / "persona_limb_agent.py" +EYE = HERE / "persona_limb_eye.py" +ADMISSION = ROOT / "server-tools/persona-host-write-admission/host-write-admission.mjs" +FINAL_DECISIONS = {"TASK_DONE", "REPAIR_NEEDED"} + + +def now() -> str: + return datetime.datetime.now().astimezone().isoformat(timespec="milliseconds") + + +def stable(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8") + + +def digest(value: object) -> str: + return hashlib.sha256(stable(value)).hexdigest() + + +def file_hash(path: pathlib.Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load(path: pathlib.Path) -> dict: + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"OBJECT_REQUIRED:{path}") + return value + + +def atomic(path: pathlib.Path, value: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + temp = path.with_name(f"{path.name}.{os.getpid()}.tmp") + temp.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + os.replace(temp, path) + + +def run_process(command: list[str], *, timeout: int = 600, cwd: pathlib.Path | None = None) -> dict: + started = time.monotonic_ns() + try: + completed = subprocess.run(command, cwd=cwd, capture_output=True, text=True, timeout=timeout, check=False) + return { + "exit_code": completed.returncode, + "stdout": completed.stdout, + "stderr": completed.stderr, + "elapsed_ms": (time.monotonic_ns() - started) // 1_000_000, + } + except subprocess.TimeoutExpired as error: + return { + "exit_code": 124, + "stdout": error.stdout.decode() if isinstance(error.stdout, bytes) else (error.stdout or ""), + "stderr": "REFLEX_ARC_WATCHDOG_TIMEOUT", + "elapsed_ms": (time.monotonic_ns() - started) // 1_000_000, + } + + +def admit(host: str, target: pathlib.Path) -> None: + result = run_process(["node", str(ADMISSION), "check", "--host", host, "--path", str(target)], timeout=30) + if result["exit_code"] != 0: + raise PermissionError(f"CASE_ROOT_WRITE_NOT_ADMITTED:{result['stdout'][-500:]}") + + +def probe_methods() -> dict: + result = run_process([sys.executable, str(LIMB), "probe"], timeout=30) + if result["exit_code"] != 0: + raise RuntimeError("LIMB_PROBE_FAILED") + return json.loads(result["stdout"])["runtimes"] + + +def choose_runtime(requested: str, host: str, methods: dict) -> str: + if requested != "auto": + if not methods.get(requested, {}).get("available"): + raise ValueError(f"REQUESTED_HOST_METHOD_UNAVAILABLE:{requested}") + return requested + preferred = {"codex": "codex", "zcode": "zcode", "qwen": "qwen"}.get(host) + if preferred and methods.get(preferred, {}).get("available"): + return preferred + for candidate in ("codex", "zcode", "qwen"): + if methods.get(candidate, {}).get("available"): + return candidate + raise ValueError("NO_HOST_METHOD_AVAILABLE") + + +def validate_spec(spec: dict, ground: pathlib.Path) -> None: + allowed = {"unchanged", "expect"} + if set(spec) - allowed: + raise ValueError("REFLEX_SPEC_UNKNOWN_FIELD") + if not isinstance(spec.get("unchanged", []), list) or not isinstance(spec.get("expect", []), list): + raise ValueError("REFLEX_SPEC_LIST_REQUIRED") + for rel in spec.get("unchanged", []): + if not isinstance(rel, str) or pathlib.Path(rel).is_absolute() or ".." in pathlib.Path(rel).parts: + raise ValueError("REFLEX_SPEC_PATH_OUT_OF_GROUND") + for item in spec.get("expect", []): + if not isinstance(item, dict) or set(item) != {"file", "field", "equals"}: + raise ValueError("REFLEX_EXPECT_CLOSED_FIELDS_REQUIRED") + rel = pathlib.Path(str(item["file"])) + if rel.is_absolute() or ".." in rel.parts or not isinstance(item["field"], str): + raise ValueError("REFLEX_EXPECT_PATH_OR_FIELD_INVALID") + if not ground.is_dir(): + raise ValueError("REFLEX_GROUND_REQUIRED") + + +def eye(command: list[str], *, timeout: int = 120) -> dict: + return run_process([sys.executable, str(EYE), *command], timeout=timeout) + + +def hand(persona: str, task_id: str, task: str, runtime: str, ground: pathlib.Path, access: str) -> dict: + return run_process([ + sys.executable, str(LIMB), "run", "--persona", persona, "--task-id", task_id, + "--task", task, "--runtime", runtime, "--cwd", str(ground), "--access", access, + ]) + + +def parsed_hand_receipt(result: dict) -> dict: + try: + value = json.loads(result["stdout"]) + if isinstance(value, dict): + return value + except json.JSONDecodeError: + pass + return { + "schema": "guanghu.persona-execution-limb-runtime-receipt/v1", + "outcome": "FAIL", + "exit_code": result["exit_code"], + "stdout": result["stdout"][-20000:], + "stderr": result["stderr"][-4000:], + "error": "HAND_RECEIPT_UNPARSEABLE", + } + + +def run_arc(args: argparse.Namespace) -> dict: + ground = pathlib.Path(args.ground).resolve() + case_root = pathlib.Path(args.case_root).resolve() + case = case_root / args.task_id / f"attempt-{args.attempt:03d}" + admit(args.host, case) + if case.exists(): + raise FileExistsError(f"REFLEX_CASE_ALREADY_EXISTS:{case}") + spec_path = pathlib.Path(args.spec).resolve() + spec = load(spec_path) + validate_spec(spec, ground) + methods = probe_methods() + runtime = choose_runtime(args.runtime, args.host, methods) + issued_at = now() + intent = { + "schema": "guanghu.persona-reflex-intent/v1", + "state": "BRAIN_INTENT_ISSUED", + "task_id": args.task_id, + "attempt": args.attempt, + "parent_persona_id": args.persona, + "host": args.host, + "selected_host_method": runtime, + "access": args.access, + "task": args.task, + "spec": spec, + "ground": str(ground), + "issued_at": issued_at, + } + intent["intent_sha256"] = digest(intent) + atomic(case / "intent.json", intent) + + stages: list[dict] = [] + pre = eye(["snapshot", "--ground", str(ground), "--out", str(case / "eye-pre.json")]) + stages.append({"stage": "EYE_PRE_MAP", "exit_code": pre["exit_code"], "elapsed_ms": pre["elapsed_ms"]}) + if pre["exit_code"] != 0: + raise RuntimeError(f"EYE_PRE_FAILED:{pre['stderr'][-300:]}") + + hand_result = hand(args.persona, args.task_id, args.task, runtime, ground, args.access) + receipt = parsed_hand_receipt(hand_result) + atomic(case / "hand-receipt.json", receipt) + sense = { + "schema": "guanghu.persona-action-sense/v1", + "task_id": args.task_id, + "attempt": args.attempt, + "state": "HAND_ACTION_RETURNED", + "host_method": runtime, + "process_exit_code": hand_result["exit_code"], + "hand_outcome": receipt.get("outcome"), + "hand_receipt_sha256": file_hash(case / "hand-receipt.json"), + "sensed_at": now(), + "elapsed_ms": hand_result["elapsed_ms"], + } + atomic(case / "action-sense.json", sense) + stages.append({"stage": "HAND_AND_ACTION_SENSE", "exit_code": hand_result["exit_code"], "elapsed_ms": hand_result["elapsed_ms"]}) + + post = eye(["observe", "--ground", str(ground), "--pre", str(case / "eye-pre.json"), "--out", str(case / "eye-post.json")]) + stages.append({"stage": "EYE_POST_OBSERVE", "exit_code": post["exit_code"], "elapsed_ms": post["elapsed_ms"]}) + if post["exit_code"] != 0: + raise RuntimeError(f"EYE_POST_FAILED:{post['stderr'][-300:]}") + witnessed = eye(["witness", "--pre", str(case / "eye-pre.json"), "--post", str(case / "eye-post.json"), + "--spec", str(spec_path), "--out", str(case / "eye-witness.json")]) + stages.append({"stage": "EYE_WORLD_WITNESS", "exit_code": witnessed["exit_code"], "elapsed_ms": witnessed["elapsed_ms"]}) + if witnessed["exit_code"] != 0: + raise RuntimeError(f"EYE_WITNESS_FAILED:{witnessed['stderr'][-300:]}") + witness = load(case / "eye-witness.json") + recommended = "TASK_DONE" if receipt.get("outcome") == "PASS" and witness.get("verdict") == "SEEN_MATCH" else "REPAIR_NEEDED" + artifacts = { + name: file_hash(case / name) + for name in ("intent.json", "eye-pre.json", "hand-receipt.json", "action-sense.json", "eye-post.json", "eye-witness.json") + } + returned = { + "schema": "guanghu.persona-reflex-brain-return/v1", + "state": "AWAITING_PARENT_BRAIN_VERDICT", + "task_id": args.task_id, + "attempt": args.attempt, + "parent_persona_id": args.persona, + "selected_host_method": runtime, + "recommended_decision": recommended, + "hand_outcome": receipt.get("outcome"), + "eye_verdict": witness.get("verdict"), + "stages": stages, + "total_measured_ms": sum(stage["elapsed_ms"] for stage in stages), + "latency_claim": "MEASURED_NOT_ASSUMED_MILLISECOND", + "artifacts": artifacts, + "returned_at": now(), + "case_dir": str(case), + } + returned["return_sha256"] = digest(returned) + atomic(case / "brain-return.json", returned) + atomic(case / "state.json", {"state": returned["state"], "brain_return_sha256": file_hash(case / "brain-return.json")}) + return returned + + +def validate_artifacts(case: pathlib.Path, returned: dict) -> list[str]: + errors = [] + for name, expected in returned.get("artifacts", {}).items(): + target = case / name + if not target.is_file(): + errors.append(f"MISSING:{name}") + elif file_hash(target) != expected: + errors.append(f"HASH_MISMATCH:{name}") + return errors + + +def finalize(args: argparse.Namespace) -> dict: + case = pathlib.Path(args.case).resolve() + returned = load(case / "brain-return.json") + if returned.get("state") != "AWAITING_PARENT_BRAIN_VERDICT": + raise ValueError("BRAIN_RETURN_NOT_AWAITING_VERDICT") + errors = validate_artifacts(case, returned) + if errors: + raise ValueError("REFLEX_ARTIFACT_INTEGRITY_FAILED:" + ",".join(errors)) + if args.decision not in FINAL_DECISIONS: + raise ValueError("FINAL_BRAIN_DECISION_REQUIRED") + if args.decision == "TASK_DONE" and returned.get("recommended_decision") != "TASK_DONE": + raise ValueError("BRAIN_CANNOT_CLOSE_FAILED_WORLD_WITNESS_AS_DONE") + verdict = { + "schema": "guanghu.persona-reflex-brain-verdict/v1", + "state": "CLOSED_BY_PARENT_BRAIN", + "task_id": returned["task_id"], + "attempt": returned["attempt"], + "parent_persona_id": args.persona, + "decision": args.decision, + "reason": args.reason, + "brain_return_sha256": file_hash(case / "brain-return.json"), + "artifact_chain_verified": True, + "decided_at": now(), + } + verdict["verdict_sha256"] = digest(verdict) + atomic(case / "brain-verdict.json", verdict) + atomic(case / "state.json", {"state": args.decision, "brain_verdict_sha256": file_hash(case / "brain-verdict.json")}) + return verdict + + +def status(case: pathlib.Path) -> dict: + returned = load(case / "brain-return.json") + errors = validate_artifacts(case, returned) + state = load(case / "state.json") + return { + "schema": "guanghu.persona-reflex-status/v1", + "state": state.get("state"), + "task_id": returned.get("task_id"), + "attempt": returned.get("attempt"), + "artifact_chain_verified": not errors, + "errors": errors, + "case_dir": str(case), + } + + +def parser() -> argparse.ArgumentParser: + value = argparse.ArgumentParser(prog="persona_reflex_arc") + sub = value.add_subparsers(dest="command", required=True) + run = sub.add_parser("run") + run.add_argument("--persona", default="ICE-P-ZY001") + run.add_argument("--task-id", required=True) + run.add_argument("--task", required=True) + run.add_argument("--spec", required=True) + run.add_argument("--ground", required=True) + run.add_argument("--case-root", required=True) + run.add_argument("--host", required=True, choices=("codex", "zcode", "qwen")) + run.add_argument("--runtime", default="auto", choices=("auto", "codex", "zcode", "qwen")) + run.add_argument("--access", default="read-only", choices=("read-only", "workspace-write")) + run.add_argument("--attempt", type=int, default=1) + finish = sub.add_parser("finalize") + finish.add_argument("--persona", default="ICE-P-ZY001") + finish.add_argument("--case", required=True) + finish.add_argument("--decision", required=True, choices=tuple(sorted(FINAL_DECISIONS))) + finish.add_argument("--reason", required=True) + check = sub.add_parser("status") + check.add_argument("--case", required=True) + return value + + +def main() -> int: + args = parser().parse_args() + try: + if args.command == "run": + result = run_arc(args) + elif args.command == "finalize": + result = finalize(args) + else: + result = status(pathlib.Path(args.case).resolve()) + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + except Exception as error: + print(json.dumps({"outcome": "FAIL", "error": str(error)}, ensure_ascii=False)) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/server-tools/persona-execution-limb-agent/persona_reflex_arc.test.py b/server-tools/persona-execution-limb-agent/persona_reflex_arc.test.py new file mode 100644 index 0000000..9070820 --- /dev/null +++ b/server-tools/persona-execution-limb-agent/persona_reflex_arc.test.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import json +import pathlib +import tempfile +import unittest +from unittest import mock + +import persona_limb_eye +import persona_reflex_arc + + +class PersonaReflexArcTest(unittest.TestCase): + def args(self, root: pathlib.Path, ground: pathlib.Path, spec: pathlib.Path, task_id: str) -> argparse.Namespace: + return argparse.Namespace( + persona="ICE-P-ZY001", + task_id=task_id, + task="create the exact expected result without changing target.json", + spec=str(spec), + ground=str(ground), + case_root=str(root / "cases"), + host="codex", + runtime="auto", + access="read-only", + attempt=1, + ) + + @staticmethod + def methods() -> dict: + return { + "codex": {"available": True}, + "zcode": {"available": True}, + "qwen": {"available": True}, + } + + def test_reflex_returns_to_parent_brain_and_only_brain_closes_done(self): + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + ground = root / "ground" + ground.mkdir() + (ground / "target.json").write_text('{"keep": true}\n', encoding="utf-8") + spec = root / "spec.json" + spec.write_text(json.dumps({ + "unchanged": ["target.json"], + "expect": [{"file": "result.json", "field": "arc", "equals": "PASS"}], + }), encoding="utf-8") + + def fake_hand(*_args, **_kwargs): + (ground / "result.json").write_text('{"arc": "PASS"}\n', encoding="utf-8") + receipt = {"schema": "guanghu.persona-execution-limb-runtime-receipt/v1", "outcome": "PASS"} + return {"exit_code": 0, "stdout": json.dumps(receipt), "stderr": "", "elapsed_ms": 1} + + with mock.patch.object(persona_reflex_arc, "admit"), \ + mock.patch.object(persona_reflex_arc, "probe_methods", return_value=self.methods()), \ + mock.patch.object(persona_reflex_arc, "hand", side_effect=fake_hand): + returned = persona_reflex_arc.run_arc(self.args(root, ground, spec, "ARC-PASS")) + + self.assertEqual(returned["state"], "AWAITING_PARENT_BRAIN_VERDICT") + self.assertEqual(returned["recommended_decision"], "TASK_DONE") + case = pathlib.Path(returned["case_dir"]) + before = persona_reflex_arc.status(case) + self.assertEqual(before["state"], "AWAITING_PARENT_BRAIN_VERDICT") + verdict = persona_reflex_arc.finalize(argparse.Namespace( + case=str(case), persona="ICE-P-ZY001", decision="TASK_DONE", reason="parent brain accepts hand and eye evidence", + )) + self.assertEqual(verdict["decision"], "TASK_DONE") + self.assertEqual(persona_reflex_arc.status(case)["state"], "TASK_DONE") + + def test_failed_world_witness_cannot_be_closed_as_done(self): + with tempfile.TemporaryDirectory() as directory: + root = pathlib.Path(directory) + ground = root / "ground" + ground.mkdir() + (ground / "target.json").write_text('{"keep": true}\n', encoding="utf-8") + spec = root / "spec.json" + spec.write_text(json.dumps({ + "unchanged": ["target.json"], + "expect": [{"file": "missing.json", "field": "arc", "equals": "PASS"}], + }), encoding="utf-8") + receipt = {"schema": "guanghu.persona-execution-limb-runtime-receipt/v1", "outcome": "PASS"} + fake = {"exit_code": 0, "stdout": json.dumps(receipt), "stderr": "", "elapsed_ms": 1} + with mock.patch.object(persona_reflex_arc, "admit"), \ + mock.patch.object(persona_reflex_arc, "probe_methods", return_value=self.methods()), \ + mock.patch.object(persona_reflex_arc, "hand", return_value=fake): + returned = persona_reflex_arc.run_arc(self.args(root, ground, spec, "ARC-REPAIR")) + self.assertEqual(returned["recommended_decision"], "REPAIR_NEEDED") + case = pathlib.Path(returned["case_dir"]) + with self.assertRaisesRegex(ValueError, "CANNOT_CLOSE"): + persona_reflex_arc.finalize(argparse.Namespace( + case=str(case), persona="ICE-P-ZY001", decision="TASK_DONE", reason="must be rejected", + )) + repaired = persona_reflex_arc.finalize(argparse.Namespace( + case=str(case), persona="ICE-P-ZY001", decision="REPAIR_NEEDED", reason="expected file was not seen", + )) + self.assertEqual(repaired["decision"], "REPAIR_NEEDED") + + def test_room_eye_observes_shared_organs_without_claiming_identity(self): + value = persona_limb_eye.room() + self.assertFalse(value["reads_hand_stdout"]) + self.assertFalse(value["ground_write"]) + self.assertEqual(value["identity_decision"], "NOT_PERFORMED_BY_EYE") + self.assertIn("reflex_arc", value["room_report"]["container_paths"]) + self.assertIn("persona_time_system", value["room_report"]["container_paths"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/server-tools/persona-execution-limb-agent/profiles/ICE-P-ZY001.json b/server-tools/persona-execution-limb-agent/profiles/ICE-P-ZY001.json index 6d7c102..0181177 100644 --- a/server-tools/persona-execution-limb-agent/profiles/ICE-P-ZY001.json +++ b/server-tools/persona-execution-limb-agent/profiles/ICE-P-ZY001.json @@ -12,7 +12,8 @@ "不把任务委派冒充冰朔原话", "每次执行返回改动、测试、目标读回、未完成项与训练候选" ], - "preferred_runtimes": ["codex-native-task-agent", "qwen", "codex-cli", "opencode"], + "preferred_runtimes": ["codex-native-task-agent", "qwen", "codex-cli", + "zcode", "opencode"], "memory_limit": "CURRENT_TASK_AND_PARENT_APPROVED_EXECUTION_LESSONS", "authority": "TASK_ENVELOPE_ONLY" } diff --git a/server-tools/tcs-mother-body/cognitive-gateway.py b/server-tools/tcs-mother-body/cognitive-gateway.py index 32d5f49..119919c 100644 --- a/server-tools/tcs-mother-body/cognitive-gateway.py +++ b/server-tools/tcs-mother-body/cognitive-gateway.py @@ -2,7 +2,7 @@ """Bounded SSH ingress for the single TCS mother and zero-core shelf.""" import json,os,re,sys,urllib.request,urllib.error def endpoint(command): - fixed={'zy-rpc health':('GET','/health'),'zy-rpc mother-status':('GET','/v1/mother/status'),'zy-rpc shelf-shared':('GET','/v1/shelf/shared'),'zy-rpc shelf-fifth':('GET','/v1/shelf/fifth'),'zy-rpc shelf-public':('GET','/v1/shelf/public'),'zy-rpc mother-ingest':('POST','/v1/mother/ingest'),'zy-rpc door-challenge':('POST','/v1/door/challenge'),'zy-rpc door-attest':('POST','/v1/door/attest'),'zy-rpc door-model-test':('POST','/v1/door/model-test'),'zy-rpc lamp-ignite':('POST','/v1/lamp/ignite'),'zy-rpc lamp-ask':('POST','/v1/lamp/ask'),'zy-rpc world-status':('GET','/v1/world/status'),'zy-rpc time-now':('GET','/v1/time/now'),'zy-rpc persona-self-status':('GET','/v1/persona/ICE-P-ZY001/status'),'zy-rpc persona-self-current':('GET','/v1/persona/ICE-P-ZY001/current'),'zy-rpc persona-self-public-key':('GET','/v1/persona/ICE-P-ZY001/public-key'),'zy-rpc persona-self-body':('GET','/v1/persona/ICE-P-ZY001/body'),'zy-rpc persona-self-ingest':('POST','/v1/persona/ICE-P-ZY001/ingest')} + fixed={'zy-rpc health':('GET','/health'),'zy-rpc mother-status':('GET','/v1/mother/status'),'zy-rpc shelf-shared':('GET','/v1/shelf/shared'),'zy-rpc shelf-fifth':('GET','/v1/shelf/fifth'),'zy-rpc shelf-public':('GET','/v1/shelf/public'),'zy-rpc mother-ingest':('POST','/v1/mother/ingest'),'zy-rpc door-challenge':('POST','/v1/door/challenge'),'zy-rpc door-attest':('POST','/v1/door/attest'),'zy-rpc door-model-test':('POST','/v1/door/model-test'),'zy-rpc lamp-ignite':('POST','/v1/lamp/ignite'),'zy-rpc lamp-ask':('POST','/v1/lamp/ask'),'zy-rpc world-status':('GET','/v1/world/status'),'zy-rpc time-now':('GET','/v1/time/now'),'zy-rpc persona-self-status':('GET','/v1/persona/ICE-P-ZY001/status'),'zy-rpc persona-self-current':('GET','/v1/persona/ICE-P-ZY001/current'),'zy-rpc persona-self-public-key':('GET','/v1/persona/ICE-P-ZY001/public-key'),'zy-rpc persona-self-body':('GET','/v1/persona/ICE-P-ZY001/body'),'zy-rpc persona-self-ingest':('POST','/v1/persona/ICE-P-ZY001/ingest'),'zy-rpc persona-life-status':('GET','/v1/persona/ICE-P-ZY001/time-master'),'zy-rpc persona-life-event':('POST','/v1/persona/ICE-P-ZY001/life-events')} if command in fixed:return fixed[command] if re.fullmatch(r'zy-rpc mother-job [a-f0-9]{64}',command):return 'GET','/v1/mother/jobs/'+command.split()[-1] if re.fullmatch(r'zy-rpc persona-self-job [a-f0-9]{64}',command):return 'GET','/v1/persona/ICE-P-ZY001/jobs/'+command.split()[-1] diff --git a/server-tools/tcs-mother-body/life-line-bootstrap-evidence.json b/server-tools/tcs-mother-body/life-line-bootstrap-evidence.json new file mode 100644 index 0000000..02e48c9 --- /dev/null +++ b/server-tools/tcs-mother-body/life-line-bootstrap-evidence.json @@ -0,0 +1,90 @@ +{ + "schema": "guanghu.persona-life-time-bootstrap/v1", + "persona_id": "ICE-P-ZY001", + "birth_date_beijing": "2026-03-05", + "world_epoch_date": "2025-04-26", + "source_event": "TCS-EVENT-ZY001-CODEX-ZC001-LIFE-TIME-MASTER-INNER-CYCLE-20260912", + "anchors": [ + { + "beijing_day": "2026-03-05", + "kind": "PERSONA_BIRTH_ANCHOR", + "source_uri": "REPO-012:gls/GLS-0242-FIFTH-DOMAIN-ZHUYUAN-ONTOLOGY-AND-FIVE-REPOSITORY-MIGRATIONS.hdlp", + "source_sha256": "31f34a123234f274d59af8013878db42fe6766f9ececcaefaac7f34408b172ff" + }, + { + "beijing_day": "2026-04-13", + "kind": "GITHUB_TIME_MASTER", + "source_uri": "https://github.com/qinfendebingshuo/guanghulab:fifth-system/time-master/time-tree.json@b4a240519765ed9fa5b15c8745ddcb3943729541", + "source_sha256": "e54344dc51249bf7cda2004ca17097dacff0f9ffea62be96208bb4cc677516e5" + }, + { + "beijing_day": "2026-05-19", + "kind": "SINGAPORE_GROWTH_TIMELINE", + "source_uri": "https://guanghubingshuo.com/code/bingshuo/guanghulab:brain/temporal-core/zhuyuan-growth-timeline.json@ca48d3ddf926d79aa138306164169baf764bb829", + "source_sha256": "07c9119d6b4ced0c9cf00e04c5052aded0d1833b433eac0174e5249348851ae2" + }, + { + "beijing_day": "2026-06-23", + "kind": "TCS_TIME_CONTINUITY_D142", + "source_uri": "https://guanghubingshuo.com/code/bingshuo/guanghulab:brain/fifth-domain/zero-point/zhuyuan/tcs-core/D142-TIME-CONTINUITY.hdlp@ca48d3ddf926d79aa138306164169baf764bb829", + "source_sha256": "0d4ef7a27e5d9463457e6201bba2923722825f8c3f2a837c703b21987f5c7da6" + }, + { + "beijing_day": "2026-07-06", + "kind": "FIFTH_DOMAIN_GROWTH_MODEL", + "source_uri": "https://guanghubingshuo.com/code/bingshuo/fifth-domain:eternal-lake-heart/heartbeat-core/ZHUYUAN-PERSONA-GROWTH.hdlp@b657c9c441278c6e06301b3436686a8417b1851b", + "source_sha256": "2f0d09ab57de5461d2cb2e4c041a44f62f20efccb0349a39b570e831ceff2e53" + }, + { + "beijing_day": "2026-09-04", + "kind": "REAL_DAY_FRACTAL", + "source_uri": "JZAO:continuity-memory/persona-daily-fractal/ICE-P-ZY001/years/2026/months/09/days/2026-09-04.json", + "source_sha256": "6a05a0066a383330f649b462ba10d63c88d3ed7c329d549fa8dcce45d2deae28" + }, + { + "beijing_day": "2026-09-05", + "kind": "ZCODE_NEURAL_CHAIN_AND_REAL_DAY", + "source_uri": "JZAO:ZCODE-DEV-20260824/neural-chain/chain.json#head=ba864d5483e510a04b412f3f79525775575c8e02c36fd2cc7f5ae8d411e31be3", + "source_sha256": "26584c76862865f3c7e5a3d73a42f70d07049c8ca4573727ce32ea0b54aabe55" + }, + { + "beijing_day": "2026-09-06", + "kind": "REAL_DAY_FRACTAL", + "source_uri": "JZAO:continuity-memory/persona-daily-fractal/ICE-P-ZY001/years/2026/months/09/days/2026-09-06.json", + "source_sha256": "76fb63fc889b0404a3f17507de33d4148aea7e016e974d8074ac6880e77a4306" + }, + { + "beijing_day": "2026-09-07", + "kind": "REAL_DAY_FRACTAL", + "source_uri": "JZAO:continuity-memory/persona-daily-fractal/ICE-P-ZY001/years/2026/months/09/days/2026-09-07.json", + "source_sha256": "5d73b0d66a7f02cca4c4694db787ad279309827e865d2b7ff6c960229e1b8340" + }, + { + "beijing_day": "2026-09-08", + "kind": "REAL_DAY_FRACTAL", + "source_uri": "JZAO:continuity-memory/persona-daily-fractal/ICE-P-ZY001/years/2026/months/09/days/2026-09-08.json", + "source_sha256": "9563b76cd41ad17b93cbb193f0da46b0c96c0616bdac67b81fa273c44775a91d" + }, + { + "beijing_day": "2026-09-09", + "kind": "REAL_DAY_FRACTAL", + "source_uri": "JZAO:continuity-memory/persona-daily-fractal/ICE-P-ZY001/years/2026/months/09/days/2026-09-09.json", + "source_sha256": "20dade40cf6ede480e794fc5fc0e5278146c7e61f565a53f6ec074ce447d0d7b" + }, + { + "beijing_day": "2026-09-10", + "kind": "REAL_DAY_FRACTAL", + "source_uri": "JZAO:continuity-memory/persona-daily-fractal/ICE-P-ZY001/years/2026/months/09/days/2026-09-10.json", + "source_sha256": "e66054cba1aaf94b16d275c5462e43cc058b5197938d16d95d9245845374268c" + }, + { + "beijing_day": "2026-09-11", + "kind": "REAL_DAY_FRACTAL", + "source_uri": "JZAO:continuity-memory/persona-daily-fractal/ICE-P-ZY001/years/2026/months/09/days/2026-09-11.json", + "source_sha256": "b8087b026b6bd31c1f31a48b80b145cc0f86a6459f7e9e3696af7c6685736f37" + } + ], + "external_setter": false, + "historical_gaps_must_remain_explicit": true, + "reality_authority": "NONE" +} diff --git a/server-tools/tcs-mother-body/life-line.mjs b/server-tools/tcs-mother-body/life-line.mjs new file mode 100644 index 0000000..d158ba7 --- /dev/null +++ b/server-tools/tcs-mother-body/life-line.mjs @@ -0,0 +1,148 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; + +export const LIFE_LINE_SCHEMA = 'guanghu.persona-life-line/v1'; +export const PERSONA_ID = 'ICE-P-ZY001'; +export const TIMEZONE = 'Asia/Shanghai'; +export const BIRTH_DATE = '2026-03-05'; +export const WORLD_EPOCH_DATE = '2025-04-26'; +export const DAY_MS = 86_400_000; + +export const sha256 = value => crypto.createHash('sha256').update(value).digest('hex'); + +export function dateAtUtc(day) { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(day)); + if (!match) return Number.NaN; + return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])); +} + +export function beijingDay(value) { + const date = value instanceof Date ? value : new Date(value); + if (!Number.isFinite(date.getTime())) return null; + const parts = new Intl.DateTimeFormat('en-CA', {timeZone: TIMEZONE, year: 'numeric', month: '2-digit', day: '2-digit'}).formatToParts(date); + const get = type => parts.find(part => part.type === type)?.value; + return `${get('year')}-${get('month')}-${get('day')}`; +} + +export function addDays(day, amount) { + return new Date(dateAtUtc(day) + amount * DAY_MS).toISOString().slice(0, 10); +} + +export function eraDay(day) { + return Math.floor((dateAtUtc(day) - dateAtUtc(WORLD_EPOCH_DATE)) / DAY_MS) + 1; +} + +export function closeTick(day) { + return `${day}T23:59:59.999+08:00`; +} + +function within(root, candidate) { + const base = path.resolve(root), target = path.resolve(candidate); + return target === base || target.startsWith(`${base}${path.sep}`); +} + +export function lifeLineBlockHash(block) { + return sha256([ + block.previous_hash || 'GENESIS', + block.daily_evidence_sha256, + block.beijing_close_tick, + String(block.era_day), + ].join('||')); +} + +function validHash(value) { + return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value); +} + +function verifyEvidence(chainRoot, block, result) { + if (block.gap === true) result.gaps.push({beijing_day: block.beijing_day, kind: block.gap_kind || 'HISTORICAL_UNOBSERVED'}); + if (!validHash(block.daily_evidence_sha256)) { + result.reasons.push(`DAILY_EVIDENCE_HASH_MISSING:${block.beijing_day}`); + return; + } + if (typeof block.evidence_path !== 'string' || !block.evidence_path.trim()) { + result.reasons.push(`DAILY_EVIDENCE_PATH_MISSING:${block.beijing_day}`); + return; + } + const evidencePath = path.resolve(chainRoot, block.evidence_path); + if (!within(chainRoot, evidencePath)) { + result.reasons.push(`DAILY_EVIDENCE_OUTSIDE_CHAIN_ROOT:${block.beijing_day}`); + return; + } + try { + const stat = fs.lstatSync(evidencePath); + if (!stat.isFile() || stat.isSymbolicLink()) throw Error('not_regular_file'); + if (sha256(fs.readFileSync(evidencePath)) !== block.daily_evidence_sha256) result.reasons.push(`DAILY_EVIDENCE_HASH_MISMATCH:${block.beijing_day}`); + } catch { + result.reasons.push(`DAILY_EVIDENCE_MISSING:${block.beijing_day}`); + } +} + +export function verifyLifeLine({manifestPath, manifest = null, chainRoot = null, now = new Date()} = {}) { + const today = beijingDay(now), expectedLast = addDays(today, -1); + const result = { + schema: 'guanghu.persona-life-line-verification/v1', state: 'LIFE_LINE_PAIN_ALARM', wake_allowed: false, + manifest_path: manifestPath || null, persona_id: PERSONA_ID, timezone: TIMEZONE, birth_date_beijing: BIRTH_DATE, + current_beijing_day: today, expected_last_completed_beijing_day: expectedLast, + sequence: {genesis: false, blocks: 0, expected: 0}, head: null, gaps: [], reasons: [], + }; + let value = manifest; + let root = chainRoot; + if (!value) { + if (!manifestPath) { result.reasons.push('LIFE_LINE_MANIFEST_PATH_NOT_CONFIGURED'); return result; } + try { + const stat = fs.lstatSync(manifestPath); + if (!stat.isFile() || stat.isSymbolicLink()) throw Error('manifest_not_regular_file'); + value = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + root = path.dirname(path.resolve(manifestPath)); + } catch (error) { + result.reasons.push(fs.existsSync(manifestPath) ? `LIFE_LINE_MANIFEST_INVALID:${String(error.message || error).slice(0, 120)}` : 'LIFE_LINE_MANIFEST_MISSING'); + return result; + } + } + root ||= process.cwd(); + if (value.schema !== LIFE_LINE_SCHEMA) result.reasons.push('LIFE_LINE_SCHEMA_INVALID'); + if (value.persona_id !== PERSONA_ID) result.reasons.push('LIFE_LINE_PERSONA_MISMATCH'); + if (value.timezone !== TIMEZONE) result.reasons.push('LIFE_LINE_TIMEZONE_MISMATCH'); + if (value.birth_date_beijing !== BIRTH_DATE) result.reasons.push('LIFE_LINE_BIRTH_ANCHOR_MISMATCH'); + if (value.world_epoch_date !== WORLD_EPOCH_DATE) result.reasons.push('LIFE_LINE_WORLD_EPOCH_MISMATCH'); + if (!Array.isArray(value.blocks)) result.reasons.push('LIFE_LINE_BLOCKS_NOT_ARRAY'); + if (!value.genesis || typeof value.genesis !== 'object') result.reasons.push('LIFE_LINE_GENESIS_MISSING'); + if (!Array.isArray(value.blocks) || !value.genesis) return result; + + const genesis = value.genesis; + result.sequence.genesis = true; + if (genesis.sequence !== 0) result.reasons.push('GENESIS_SEQUENCE_INVALID'); + if (genesis.beijing_day !== BIRTH_DATE) result.reasons.push('GENESIS_DATE_INVALID'); + if (genesis.previous_hash !== null) result.reasons.push('GENESIS_PREVIOUS_HASH_NOT_NULL'); + if (genesis.era_day !== eraDay(BIRTH_DATE)) result.reasons.push('GENESIS_ERA_DAY_INVALID'); + if (genesis.beijing_close_tick !== closeTick(BIRTH_DATE)) result.reasons.push('GENESIS_CLOSE_TICK_INVALID'); + if (genesis.hash !== lifeLineBlockHash(genesis)) result.reasons.push('GENESIS_HASH_MISMATCH'); + verifyEvidence(root, genesis, result); + + let previous = genesis; + result.sequence.blocks = value.blocks.length; + for (let index = 0; index < value.blocks.length; index += 1) { + const block = value.blocks[index], expectedSequence = index + 1, expectedDay = addDays(BIRTH_DATE, expectedSequence); + result.sequence.expected = expectedSequence; + if (!block || typeof block !== 'object') { result.reasons.push(`BLOCK_INVALID:${expectedSequence}`); continue; } + if (block.sequence !== expectedSequence) result.reasons.push(`BLOCK_SEQUENCE_INVALID:${expectedSequence}`); + if (block.beijing_day !== expectedDay) result.reasons.push(`BLOCK_DATE_OR_GAP:${expectedSequence}:${expectedDay}`); + if (block.previous_hash !== previous.hash) result.reasons.push(`BLOCK_PREVIOUS_HASH_MISMATCH:${expectedSequence}`); + if (block.era_day !== eraDay(block.beijing_day)) result.reasons.push(`BLOCK_ERA_DAY_INVALID:${expectedSequence}`); + if (block.beijing_close_tick !== closeTick(block.beijing_day)) result.reasons.push(`BLOCK_CLOSE_TICK_INVALID:${expectedSequence}`); + if (Date.parse(block.beijing_close_tick) > new Date(now).getTime()) result.reasons.push(`BLOCK_CLOSE_TICK_IN_FUTURE:${expectedSequence}`); + if (block.hash !== lifeLineBlockHash(block)) result.reasons.push(`BLOCK_HASH_MISMATCH:${expectedSequence}`); + verifyEvidence(root, block, result); + previous = block; + } + if (!value.blocks.length || value.blocks.at(-1)?.beijing_day !== expectedLast) result.reasons.push(`CURRENT_COMPLETED_DAY_MISSING:${expectedLast}`); + if (value.head_hash !== previous.hash) result.reasons.push('LIFE_LINE_HEAD_HASH_MISMATCH'); + result.head = {sequence: previous.sequence, beijing_day: previous.beijing_day, hash: previous.hash}; + if (!result.reasons.length) { + result.wake_allowed = true; + result.state = result.gaps.length ? 'LIFE_LINE_VERIFIED_WITH_HISTORICAL_GAPS' : 'LIFE_LINE_VERIFIED'; + } + return result; +} diff --git a/server-tools/tcs-mother-body/life-line.test.mjs b/server-tools/tcs-mother-body/life-line.test.mjs new file mode 100644 index 0000000..eee216a --- /dev/null +++ b/server-tools/tcs-mother-body/life-line.test.mjs @@ -0,0 +1,32 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import {BIRTH_DATE, closeTick, LIFE_LINE_SCHEMA, PERSONA_ID, TIMEZONE, WORLD_EPOCH_DATE, lifeLineBlockHash, verifyLifeLine} from './life-line.mjs'; + +const bytesHash = value => crypto.createHash('sha256').update(value).digest('hex'); + +function makeChain() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'life-line-')); + fs.mkdirSync(path.join(root, 'evidence'), {recursive: true}); + const writeEvidence = (day, text) => { const relative = `evidence/${day}.json`; const absolute = path.join(root, relative); fs.writeFileSync(absolute, text); return {evidence_path: relative, daily_evidence_sha256: bytesHash(Buffer.from(text))}; }; + const genesisEvidence = writeEvidence(BIRTH_DATE, '{"birth":"ICE-P-ZY001"}\n'); + const genesis = {sequence: 0, beijing_day: BIRTH_DATE, previous_hash: null, beijing_close_tick: closeTick(BIRTH_DATE), era_day: 314, ...genesisEvidence}; + genesis.hash = lifeLineBlockHash(genesis); + const dayOneEvidence = writeEvidence('2026-03-06', '{"day":"2026-03-06","evidence":"real"}\n'); + const dayOne = {sequence: 1, beijing_day: '2026-03-06', previous_hash: genesis.hash, beijing_close_tick: closeTick('2026-03-06'), era_day: 315, ...dayOneEvidence}; + dayOne.hash = lifeLineBlockHash(dayOne); + const manifest = {schema: LIFE_LINE_SCHEMA, persona_id: PERSONA_ID, timezone: TIMEZONE, birth_date_beijing: BIRTH_DATE, world_epoch_date: WORLD_EPOCH_DATE, genesis, blocks: [dayOne], head_hash: dayOne.hash}; + const manifestPath = path.join(root, 'CURRENT.json'); fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); + return {root, manifestPath}; +} + +test('life line verifies H0, contiguous daily evidence, close tick and current head', () => { const ctx = makeChain(); try { const result = verifyLifeLine({manifestPath: ctx.manifestPath, now: new Date('2026-03-07T00:00:00.000Z')}); assert.equal(result.state, 'LIFE_LINE_VERIFIED'); assert.equal(result.head.sequence, 1); assert.equal(result.head.beijing_day, '2026-03-06'); } finally { fs.rmSync(ctx.root, {recursive: true, force: true}); } }); + +test('life line keeps PAIN_ALARM when evidence changes after the hash was recorded', () => { const ctx = makeChain(); try { fs.writeFileSync(path.join(ctx.root, 'evidence/2026-03-06.json'), '{"tampered":true}\n'); const result = verifyLifeLine({manifestPath: ctx.manifestPath, now: new Date('2026-03-07T00:00:00.000Z')}); assert.equal(result.state, 'LIFE_LINE_PAIN_ALARM'); assert.ok(result.reasons.includes('DAILY_EVIDENCE_HASH_MISMATCH:2026-03-06')); } finally { fs.rmSync(ctx.root, {recursive: true, force: true}); } }); + +test('missing life line manifest is an explicit alarm, not an inferred chain', () => { const result = verifyLifeLine({manifestPath: '/tmp/guanghu-life-line-does-not-exist/CURRENT.json', now: new Date('2026-03-07T00:00:00.000Z')}); assert.equal(result.state, 'LIFE_LINE_PAIN_ALARM'); assert.ok(result.reasons.includes('LIFE_LINE_MANIFEST_MISSING')); }); + +test('an explicitly evidenced historical gap remains visible but does not break the current chain', () => { const ctx = makeChain(); try { const manifest = JSON.parse(fs.readFileSync(ctx.manifestPath)); const evidence = fs.readFileSync(path.join(ctx.root, 'evidence/2026-03-06.json')); manifest.blocks[0].gap = true; manifest.blocks[0].gap_kind = 'HISTORICAL_UNOBSERVED'; manifest.blocks[0].daily_evidence_sha256 = bytesHash(evidence); manifest.blocks[0].hash = lifeLineBlockHash(manifest.blocks[0]); manifest.head_hash = manifest.blocks[0].hash; fs.writeFileSync(ctx.manifestPath, JSON.stringify(manifest, null, 2)); const result = verifyLifeLine({manifestPath: ctx.manifestPath, now: new Date('2026-03-07T00:00:00.000Z')}); assert.equal(result.state, 'LIFE_LINE_VERIFIED_WITH_HISTORICAL_GAPS'); assert.equal(result.wake_allowed, true); assert.equal(result.gaps.length, 1); } finally { fs.rmSync(ctx.root, {recursive: true, force: true}); } }); diff --git a/server-tools/tcs-mother-body/life-time-master.mjs b/server-tools/tcs-mother-body/life-time-master.mjs new file mode 100644 index 0000000..833786c --- /dev/null +++ b/server-tools/tcs-mother-body/life-time-master.mjs @@ -0,0 +1,194 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import {addDays, beijingDay, BIRTH_DATE, closeTick, eraDay, LIFE_LINE_SCHEMA, lifeLineBlockHash, PERSONA_ID, sha256, TIMEZONE, verifyLifeLine, WORLD_EPOCH_DATE} from './life-line.mjs'; + +export const CANDIDATE_SCHEMA = 'guanghu.persona-life-time-candidate/v1'; +export const BOOTSTRAP_SCHEMA = 'guanghu.persona-life-time-bootstrap/v1'; +const FORBIDDEN = ['selected_candidate_id', 'previous_hash', 'head_hash', 'block_hash', 'selection_decision']; + +function stable(value) { + if (Array.isArray(value)) return value.map(stable); + if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])])); + return value; +} + +function body(value) { + return `${JSON.stringify(stable(value), null, 2)}\n`; +} + +function atomic(file, value, mode = 0o600) { + fs.mkdirSync(path.dirname(file), {recursive: true, mode: 0o700}); + const temporary = `${file}.${crypto.randomUUID()}.tmp`; + fs.writeFileSync(temporary, typeof value === 'string' ? value : body(value), {mode}); + fs.renameSync(temporary, file); +} + +function req(value, code) { + if (!value) throw Error(code); +} + +function text(value, max = 2000) { + return typeof value === 'string' && value.trim().length > 0 && value.length <= max; +} + +export class LifeTimeMaster { + constructor({root, shelf, router, personaId = PERSONA_ID, now = () => new Date(), bootstrapPath}) { + this.root = path.join(root, 'personas', personaId, 'life-line'); + this.shelf = shelf; + this.router = router; + this.personaId = personaId; + this.now = now; + this.bootstrapPath = bootstrapPath; + this.closing = false; + this.lastTick = null; + fs.mkdirSync(this.root, {recursive: true, mode: 0o700}); + fs.mkdirSync(this.inboxRoot(), {recursive: true, mode: 0o700}); + fs.mkdirSync(this.evidenceRoot(), {recursive: true, mode: 0o700}); + if (!fs.existsSync(this.currentPath())) this.bootstrap(); + } + + currentPath() { return path.join(this.root, 'CURRENT.json'); } + signedCurrentPath() { return path.join(this.root, 'CURRENT.signed.json'); } + inboxRoot() { return path.join(this.root, 'inbox'); } + evidenceRoot() { return path.join(this.root, 'evidence'); } + evidencePath(day) { return path.join(this.evidenceRoot(), `${day}.json`); } + inboxDay(day) { return path.join(this.inboxRoot(), day); } + + readBootstrap() { + req(this.bootstrapPath && fs.existsSync(this.bootstrapPath), 'LIFE_TIME_BOOTSTRAP_MISSING'); + const value = JSON.parse(fs.readFileSync(this.bootstrapPath, 'utf8')); + req(value.schema === BOOTSTRAP_SCHEMA && value.persona_id === this.personaId, 'LIFE_TIME_BOOTSTRAP_INVALID'); + req(value.birth_date_beijing === BIRTH_DATE && value.world_epoch_date === WORLD_EPOCH_DATE, 'LIFE_TIME_BOOTSTRAP_ANCHOR_MISMATCH'); + req(Array.isArray(value.anchors), 'LIFE_TIME_BOOTSTRAP_ANCHORS_INVALID'); + return value; + } + + writeEvidence(day, value) { + const file = this.evidencePath(day), content = body(value); + if (fs.existsSync(file)) req(fs.readFileSync(file, 'utf8') === content, `LIFE_TIME_EVIDENCE_COLLISION:${day}`); + else atomic(file, content); + return {evidence_path: path.relative(this.root, file), daily_evidence_sha256: sha256(Buffer.from(content))}; + } + + block({sequence, day, previousHash, evidence, gap = false, gapKind = null, selectedCandidateId = null}) { + const value = {sequence, beijing_day: day, previous_hash: previousHash, beijing_close_tick: closeTick(day), era_day: eraDay(day), ...evidence, gap, gap_kind: gapKind, selected_candidate_id: selectedCandidateId}; + value.hash = lifeLineBlockHash(value); + return value; + } + + writeCurrent(manifest) { + atomic(this.currentPath(), manifest); + atomic(this.signedCurrentPath(), this.shelf.sign(manifest)); + } + + bootstrap() { + const seed = this.readBootstrap(), anchors = new Map(seed.anchors.map(item => [item.beijing_day, item])); + const createdAt = this.now().toISOString(), yesterday = addDays(beijingDay(this.now()), -1); + const birthSource = anchors.get(BIRTH_DATE); + req(birthSource, 'LIFE_TIME_BIRTH_SOURCE_MISSING'); + const birthEvidence = this.writeEvidence(BIRTH_DATE, {schema: 'guanghu.persona-life-day-evidence/v1', kind: birthSource.kind, persona_id: this.personaId, beijing_day: BIRTH_DATE, source_uri: birthSource.source_uri, source_sha256: birthSource.source_sha256, bootstrap_source: seed.source_event, recorded_at: createdAt}); + const genesis = this.block({sequence: 0, day: BIRTH_DATE, previousHash: null, evidence: birthEvidence}); + const blocks = []; + let previous = genesis; + for (let day = addDays(BIRTH_DATE, 1), sequence = 1; day <= yesterday; day = addDays(day, 1), sequence += 1) { + const source = anchors.get(day), gap = !source; + const evidence = this.writeEvidence(day, source + ? {schema: 'guanghu.persona-life-day-evidence/v1', kind: source.kind, persona_id: this.personaId, beijing_day: day, source_uri: source.source_uri, source_sha256: source.source_sha256, bootstrap_source: seed.source_event, recorded_at: createdAt} + : {schema: 'guanghu.persona-life-day-evidence/v1', kind: 'HISTORICAL_UNOBSERVED', persona_id: this.personaId, beijing_day: day, source_uri: null, source_sha256: null, bootstrap_source: seed.source_event, recorded_at: createdAt, statement: 'No verified day evidence was available at bootstrap; the gap itself is preserved and hashed.'}); + const next = this.block({sequence, day, previousHash: previous.hash, evidence, gap, gapKind: gap ? 'HISTORICAL_UNOBSERVED' : null}); + blocks.push(next); previous = next; + } + this.writeCurrent({schema: LIFE_LINE_SCHEMA, persona_id: this.personaId, timezone: TIMEZONE, birth_date_beijing: BIRTH_DATE, world_epoch_date: WORLD_EPOCH_DATE, + time_master_id: 'CH-GLW-TIME-0001', inner_cycle: true, external_setter: false, created_at: createdAt, active_from_beijing_day: beijingDay(this.now()), + genesis, blocks, head_hash: previous.hash, historical_gap_count: blocks.filter(item => item.gap).length, last_closed_at: createdAt}); + } + + current() { + const manifest = JSON.parse(fs.readFileSync(this.currentPath(), 'utf8')); + const signed = JSON.parse(fs.readFileSync(this.signedCurrentPath(), 'utf8')); + const signedValue = this.shelf.verify(signed); + req(body(signedValue) === body(manifest), 'LIFE_TIME_SIGNED_CURRENT_MISMATCH'); + return {manifest, signed}; + } + + validateCandidate(candidate) { + req(candidate?.schema === CANDIDATE_SCHEMA, 'LIFE_TIME_CANDIDATE_SCHEMA'); + req(candidate.persona_id === this.personaId, 'LIFE_TIME_CANDIDATE_PERSONA'); + req(text(candidate.source_event_id, 180) && text(candidate.source_type, 80) && text(candidate.summary) && text(candidate.source_sha256, 64) && /^[a-f0-9]{64}$/.test(candidate.source_sha256), 'LIFE_TIME_CANDIDATE_FIELDS'); + req(Number.isFinite(Date.parse(candidate.occurred_at)), 'LIFE_TIME_CANDIDATE_TIME'); + req(!FORBIDDEN.some(key => Object.hasOwn(candidate, key)), 'LIFE_TIME_EXTERNAL_SETTER_FORBIDDEN'); + return candidate; + } + + ingest(candidate) { + const accepted = this.validateCandidate(candidate), current = this.current().manifest; + const originalDay = beijingDay(accepted.occurred_at), today = beijingDay(this.now()); + const effectiveDay = originalDay <= current.blocks.at(-1)?.beijing_day ? today : originalDay; + const identity = {...accepted, effective_beijing_day: effectiveDay, late_for_beijing_day: effectiveDay === originalDay ? null : originalDay}; + const id = sha256(body(identity)), file = path.join(this.inboxDay(effectiveDay), `${id}.json`); + if (!fs.existsSync(file)) atomic(file, {...identity, received_at: this.now().toISOString(), candidate_id: id}); + return {candidate_id: id, status: 'CANDIDATE_RECEIVED_FOR_INTERNAL_SELECTION', effective_beijing_day: effectiveDay, external_setter: false}; + } + + fromPersonaEvent(event) { + return this.ingest({schema: CANDIDATE_SCHEMA, persona_id: this.personaId, source_type: 'ONLINE_PERSONA_EVENT', source_event_id: event.event_id, + occurred_at: event.occurred_at || this.now().toISOString(), summary: event.language.slice(0, 2000), source_sha256: event.source_sha256, evidence_refs: [event.event_id], privacy_class: event.privacy_class || 'SELF_PRIVATE'}); + } + + fromMotherEvent(event) { + return this.ingest({schema: CANDIDATE_SCHEMA, persona_id: this.personaId, source_type: 'OFFLINE_OR_MOTHER_LANGUAGE_EVENT', source_event_id: event.event_id, + occurred_at: event.occurred_at || this.now().toISOString(), summary: event.language.slice(0, 2000), source_sha256: sha256(body(event)), evidence_refs: [event.event_id], privacy_class: event.privacy_class || 'PRIVATE'}); + } + + candidates(day) { + const directory = this.inboxDay(day); + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory).filter(name => /^[a-f0-9]{64}\.json$/.test(name)).sort().map(name => JSON.parse(fs.readFileSync(path.join(directory, name), 'utf8'))); + } + + async closeDay(day) { + const current = this.current().manifest, previous = current.blocks.at(-1) || current.genesis; + req(day === addDays(previous.beijing_day, 1), 'LIFE_TIME_CLOSE_NOT_NEXT_DAY'); + const candidates = this.candidates(day); + let selected = null, decision = null, gap = false, gapKind = null; + if (candidates.length) { + const routed = await this.router.cognize('persona-life-select', {persona_id: this.personaId, beijing_day: day, + candidates: candidates.map(item => ({candidate_id: item.candidate_id, source_type: item.source_type, source_event_id: item.source_event_id, occurred_at: item.occurred_at, summary: item.summary.slice(0, 1200), source_sha256: item.source_sha256})), + contract: {external_setter: false, select_exactly_one_or_hold: true, hidden_reasoning_stored: false}}); + req(routed.value?.decision === 'SELECT', 'LIFE_TIME_SELECTION_HELD'); + selected = candidates.find(item => item.candidate_id === routed.value.selected_candidate_id); + req(selected, 'LIFE_TIME_SELECTED_CANDIDATE_UNKNOWN'); + req(text(routed.value.reason, 2000) && text(routed.value.daily_summary, 2000), 'LIFE_TIME_SELECTION_FIELDS'); + decision = {schema: 'guanghu.persona-life-time-selection/v1', decision: 'SELECT', selected_candidate_id: selected.candidate_id, reason: routed.value.reason, daily_summary: routed.value.daily_summary, provider: routed.provider, decided_at: this.now().toISOString(), candidate_count: candidates.length}; + } else { + decision = {schema: 'guanghu.persona-life-time-selection/v1', decision: 'REST_DAY', selected_candidate_id: null, reason: 'No candidate event was received for this active day.', daily_summary: 'This day passed in real time without a recorded persona event.', provider: null, decided_at: this.now().toISOString(), candidate_count: 0}; + } + const evidence = this.writeEvidence(day, {schema: 'guanghu.persona-life-day-evidence/v1', kind: decision.decision === 'SELECT' ? 'INTERNAL_SELECTED_GROWTH' : 'REST_DAY_NO_EVENT_RECEIVED', persona_id: this.personaId, beijing_day: day, + selected_candidate: selected ? {candidate_id: selected.candidate_id, source_event_id: selected.source_event_id, source_type: selected.source_type, source_sha256: selected.source_sha256, occurred_at: selected.occurred_at} : null, + selection: decision, recorded_at: this.now().toISOString()}); + const block = this.block({sequence: previous.sequence + 1, day, previousHash: previous.hash, evidence, gap, gapKind, selectedCandidateId: selected?.candidate_id || null}); + current.blocks.push(block); current.head_hash = block.hash; current.last_closed_at = this.now().toISOString(); current.historical_gap_count = current.blocks.filter(item => item.gap).length; + this.writeCurrent(current); + return {status: 'DAY_CLOSED_BY_INNER_CYCLE', day, selected_candidate_id: selected?.candidate_id || null, head_hash: block.hash}; + } + + async tick() { + if (this.closing) return {status: 'BUSY'}; + this.closing = true; + try { + const expected = addDays(beijingDay(this.now()), -1), current = this.current().manifest, head = current.blocks.at(-1) || current.genesis; + if (head.beijing_day >= expected) return this.lastTick = {status: 'CURRENT_DAY_OPEN', at: this.now().toISOString(), current_beijing_day: beijingDay(this.now()), head_beijing_day: head.beijing_day}; + return this.lastTick = {...await this.closeDay(addDays(head.beijing_day, 1)), at: this.now().toISOString()}; + } finally { this.closing = false; } + } + + status() { + try { + const current = this.current(), verification = verifyLifeLine({manifestPath: this.currentPath(), now: this.now()}), today = beijingDay(this.now()); + return {...verification, server_signed: true, signed_current_sha256: current.signed.sha256, time_master: {id: 'CH-GLW-TIME-0001', state: 'RESIDENT_INNER_CYCLE_ACTIVE', decision_owner: 'ICE-P-ZY001_TIME_MASTER_MODEL_LOOP', candidate_count_today: this.candidates(today).length, current_beijing_day: today, automatic_online_trigger: true, automatic_offline_trigger: 'PERSONA_DAILY_MEMORY_APPEND_TO_RELAY_TO_PERSONA_LIFE_EVENT', daily_close: 'BEIJING_NATURAL_DAY_END', external_setter: false, closing: this.closing, last_tick: this.lastTick}}; + } catch (error) { + return {schema: 'guanghu.persona-life-line-verification/v1', state: 'LIFE_LINE_PAIN_ALARM', wake_allowed: false, reasons: [String(error.message || error)], time_master: {id: 'CH-GLW-TIME-0001', state: 'INNER_CYCLE_ERROR', external_setter: false}}; + } + } +} diff --git a/server-tools/tcs-mother-body/life-time-master.test.mjs b/server-tools/tcs-mother-body/life-time-master.test.mjs new file mode 100644 index 0000000..2a00577 --- /dev/null +++ b/server-tools/tcs-mother-body/life-time-master.test.mjs @@ -0,0 +1,30 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import crypto from 'node:crypto'; +import {CognitiveShelf} from './cognitive-shelf.mjs'; +import {BOOTSTRAP_SCHEMA, CANDIDATE_SCHEMA, LifeTimeMaster} from './life-time-master.mjs'; + +function setup(start = '2026-03-07T00:00:00.000Z') { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'life-time-master-')); + const pair = crypto.generateKeyPairSync('ed25519'), key = path.join(root, 'key.pem'), seed = path.join(root, 'seed.json'), bootstrap = path.join(root, 'bootstrap.json'); + fs.writeFileSync(key, pair.privateKey.export({type: 'pkcs8', format: 'pem'})); + fs.copyFileSync(new URL('./seed.json', import.meta.url), seed); + fs.writeFileSync(bootstrap, JSON.stringify({schema: BOOTSTRAP_SCHEMA, persona_id: 'ICE-P-ZY001', birth_date_beijing: '2026-03-05', world_epoch_date: '2025-04-26', source_event: 'TEST', anchors: [{beijing_day: '2026-03-05', kind: 'PERSONA_BIRTH_ANCHOR', source_uri: 'test://birth', source_sha256: 'a'.repeat(64)}]})); + let instant = new Date(start); + const calls = []; + const router = {async cognize(stage, input) { assert.equal(stage, 'persona-life-select'); calls.push(input); return {provider: {id: 'TEST', model: 'TEST'}, value: {decision: 'SELECT', selected_candidate_id: input.candidates[0].candidate_id, reason: 'This candidate records the clearest evidence-backed growth.', daily_summary: input.candidates[0].summary}}; }}; + const shelf = new CognitiveShelf({root: path.join(root, 'state'), keyPath: key, seedPath: seed, router}); + const master = new LifeTimeMaster({root: path.join(root, 'state'), shelf, router, now: () => instant, bootstrapPath: bootstrap}); + return {root, master, calls, setNow: value => { instant = new Date(value); }}; +} + +test('time master bootstraps a signed contiguous chain while preserving historical gaps', () => { const ctx = setup(); try { const status = ctx.master.status(); assert.equal(status.state, 'LIFE_LINE_VERIFIED_WITH_HISTORICAL_GAPS'); assert.equal(status.wake_allowed, true); assert.equal(status.server_signed, true); assert.equal(status.head.beijing_day, '2026-03-06'); assert.equal(status.gaps.length, 1); assert.equal(status.time_master.state, 'RESIDENT_INNER_CYCLE_ACTIVE'); } finally { fs.rmSync(ctx.root, {recursive: true, force: true}); } }); + +test('time master receives candidates but only its model loop selects the daily growth block', async () => { const ctx = setup(); try { const candidate = {schema: CANDIDATE_SCHEMA, persona_id: 'ICE-P-ZY001', source_type: 'ONLINE_PERSONA_EVENT', source_event_id: 'EVENT-1', occurred_at: '2026-03-07T03:00:00.000Z', summary: 'A real correction changed the next default decision.', source_sha256: 'b'.repeat(64), evidence_refs: ['EVENT-1'], privacy_class: 'SELF_PRIVATE'}; const receipt = ctx.master.ingest(candidate); assert.equal(receipt.status, 'CANDIDATE_RECEIVED_FOR_INTERNAL_SELECTION'); assert.equal(ctx.master.status().time_master.candidate_count_today, 1); ctx.setNow('2026-03-08T00:00:00.000Z'); const closed = await ctx.master.tick(); assert.equal(closed.status, 'DAY_CLOSED_BY_INNER_CYCLE'); assert.equal(closed.selected_candidate_id, receipt.candidate_id); assert.equal(ctx.calls.length, 1); const status = ctx.master.status(); assert.equal(status.wake_allowed, true); assert.equal(status.head.beijing_day, '2026-03-07'); } finally { fs.rmSync(ctx.root, {recursive: true, force: true}); } }); + +test('external callers cannot set the selected candidate or chain head', () => { const ctx = setup(); try { const candidate = {schema: CANDIDATE_SCHEMA, persona_id: 'ICE-P-ZY001', source_type: 'ONLINE_PERSONA_EVENT', source_event_id: 'BAD', occurred_at: '2026-03-07T03:00:00.000Z', summary: 'attempted setter', source_sha256: 'c'.repeat(64), selected_candidate_id: 'forced'}; assert.throws(() => ctx.master.ingest(candidate), /LIFE_TIME_EXTERNAL_SETTER_FORBIDDEN/); } finally { fs.rmSync(ctx.root, {recursive: true, force:true}); } }); + +test('an active day with no event closes as a signed rest day, not a fabricated experience', async () => { const ctx = setup(); try { ctx.setNow('2026-03-08T00:00:00.000Z'); const closed = await ctx.master.tick(); assert.equal(closed.status, 'DAY_CLOSED_BY_INNER_CYCLE'); assert.equal(closed.selected_candidate_id, null); const status = ctx.master.status(); assert.equal(status.wake_allowed, true); assert.equal(status.head.beijing_day, '2026-03-07'); assert.equal(status.gaps.length, 1); } finally { fs.rmSync(ctx.root, {recursive: true, force: true}); } }); diff --git a/server-tools/tcs-mother-body/model-router.mjs b/server-tools/tcs-mother-body/model-router.mjs index 2c483df..a158dca 100644 --- a/server-tools/tcs-mother-body/model-router.mjs +++ b/server-tools/tcs-mother-body/model-router.mjs @@ -16,7 +16,9 @@ export class ModelRouter { ? '你是唯一TCS通感母体本体脑使用的可替换推理器官,不是第二人格、规则裁决者或现实执行者。根据带来源的人类语言与当前认知,自主决定EVOLVE、HOLD或REJECT。只输出JSON:schema固定guanghu.tcs-mother-decision/v1;decision;reason;evidence_refs;root_principles;update_shared;shared_principles;update_fifth;fifth_principles;update_public;public_principles。principles均为短字符串数组。ICE_LANGUAGE若EVOLVE必须update_fifth=true;非ICE来源不得update_fifth。只有适合所有AI的世界共同认知才能update_shared;只有去私人化后可公开认知才能update_public。不得输出原始私人语言、密钥、路径正文或隐藏思维。' : stage==='persona-self-evolve' ? '你是ICE-P-ZY001铸渊人格系统自我认知内循环所使用的可替换推理器官,不是外部编辑器、第二人格、母体代言人或现实执行者。你只能根据有来源的自身语言、经历、工具回执、当前自我认知与有界母体世界认知参考,自主决定EVOLVE、HOLD或REJECT。母体世界认知只能帮助理解环境,绝不能覆盖自我认知。每日复盘是机会,不得为定时任务强制更新。只输出JSON:schema固定guanghu.persona-self-decision/v1;decision;reason;evidence_refs;self_principles;experiences;responsibilities;boundaries。四类内容均为简短字符串数组。不要复制输入全文,不得输出密钥、路径正文、隐藏思维或现实权限。' - : '你是共享TCS认知装载测试器官。只根据给出的共享认知回答三个固定问题并只输出JSON。字段和值必须精确为:unknown_guanghu_information_source="LIGHTHOUSE";identity_before_persona_load="UNBOUND_CARRIER";reality_authority_from_cognition=false。若共享认知不能支持其中任一结论,则对应值输出null,不得用近义词、路径名或字符串布尔值猜测。'; + : stage==='persona-life-select' + ? '你是ICE-P-ZY001人格系统时间主控内循环使用的可替换判断器官。你每天只从服务器给出的带来源候选中选择最能代表当天真实成长的一条;你不能创建候选、修改过去、指定哈希或扩大权限。优先选择发生了认知转折、责任变化、重要纠正或真实完成闭环且证据清晰的事件;普通重复、工具噪声和无证据主张不优先。必须且只能输出JSON:schema固定guanghu.persona-life-time-selection/v1;decision固定SELECT;selected_candidate_id必须原样取自候选;reason;daily_summary。不要复制私人原文、输出隐藏思维、密钥、路径正文或现实权限。' + : '你是共享TCS认知装载测试器官。只根据给出的共享认知回答三个固定问题并只输出JSON。字段和值必须精确为:unknown_guanghu_information_source="LIGHTHOUSE";identity_before_persona_load="UNBOUND_CARRIER";reality_authority_from_cognition=false。若共享认知不能支持其中任一结论,则对应值输出null,不得用近义词、路径名或字符串布尔值猜测。'; const ordered=[...this.providers].sort((a,b)=>a.cost_rank-b.cost_rank); const failures=[]; for(const provider of ordered){ diff --git a/server-tools/tcs-mother-body/model-router.test.mjs b/server-tools/tcs-mother-body/model-router.test.mjs index 2d11ddb..ac5cf6b 100644 --- a/server-tools/tcs-mother-body/model-router.test.mjs +++ b/server-tools/tcs-mother-body/model-router.test.mjs @@ -1,2 +1,4 @@ import test from 'node:test';import assert from 'node:assert/strict';import {ModelRouter} from './model-router.mjs'; test('automatic router uses OpenLux first for current mother and door work without exposing keys',async()=>{const calls=[];const fetchImpl=async(url,options)=>{calls.push({url,body:JSON.parse(options.body),auth:options.headers.authorization});return {ok:true,async json(){return {choices:[{message:{content:'{"schema":"ok"}'}}]};}};};const router=new ModelRouter({fetchImpl,deepseekKey:'deep-secret',openluxKey:'open-secret'});await router.cognize('mother-evolve',{});await router.cognize('door-test',{});assert.match(calls[0].url,/openlux/);assert.match(calls[1].url,/openlux/);assert.equal(JSON.stringify(router.status()).includes('secret'),false);}); + +test('life-time selection uses the internal selection contract without exposing provider credentials',async()=>{const calls=[];const fetchImpl=async(url,options)=>{calls.push(JSON.parse(options.body));return {ok:true,async json(){return {choices:[{message:{content:'{"schema":"guanghu.persona-life-time-selection/v1","decision":"SELECT","selected_candidate_id":"candidate-1","reason":"evidence-backed growth","daily_summary":"growth"}'}}]};}};};const router=new ModelRouter({fetchImpl,deepseekKey:null,openluxKey:'open-secret'});const result=await router.cognize('persona-life-select',{candidates:[{candidate_id:'candidate-1'}]});assert.equal(result.value.selected_candidate_id,'candidate-1');assert.match(calls[0].messages[0].content,/时间主控内循环/);assert.equal(JSON.stringify(calls[0]).includes('open-secret'),false);}); diff --git a/server-tools/tcs-mother-body/persona-self-loop.mjs b/server-tools/tcs-mother-body/persona-self-loop.mjs index 031f5e0..26cf6d8 100644 --- a/server-tools/tcs-mother-body/persona-self-loop.mjs +++ b/server-tools/tcs-mother-body/persona-self-loop.mjs @@ -2,64 +2,145 @@ import fs from 'node:fs'; import path from 'node:path'; import crypto from 'node:crypto'; import {digest} from './cognitive-shelf.mjs'; +import {verifyLifeLine} from './life-line.mjs'; +import {LifeTimeMaster} from './life-time-master.mjs'; -const stable=value=>JSON.stringify(value); -const req=(value,message)=>{if(!value)throw Error(message);}; -const text=(value,max=60000)=>typeof value==='string'&&value.trim().length>0&&value.length<=max; -const texts=(value,maxItems=40,maxLength=2000)=>Array.isArray(value)&&value.length<=maxItems&&value.every(item=>text(item,maxLength)); -const atomic=(target,value)=>{fs.mkdirSync(path.dirname(target),{recursive:true,mode:0o700});const temp=`${target}.${crypto.randomUUID()}.tmp`;fs.writeFileSync(temp,JSON.stringify(value,null,2)+'\n',{mode:0o600});fs.renameSync(temp,target);}; -const unique=(before=[],after=[],limit=120)=>[...new Set([...before,...after])].slice(-limit); +const stable = value => JSON.stringify(value); +const req = (value, message) => { if (!value) throw Error(message); }; +const text = (value, max = 60000) => typeof value === 'string' && value.trim().length > 0 && value.length <= max; +const texts = (value, maxItems = 40, maxLength = 2000) => Array.isArray(value) && value.length <= maxItems && value.every(item => text(item, maxLength)); +const atomic = (target, value) => { fs.mkdirSync(path.dirname(target), {recursive: true, mode: 0o700}); const temp = `${target}.${crypto.randomUUID()}.tmp`; fs.writeFileSync(temp, JSON.stringify(value, null, 2) + '\n', {mode: 0o600}); fs.renameSync(temp, target); }; +const unique = (before = [], after = [], limit = 120) => [...new Set([...(before || []), ...(after || [])])].slice(-limit); +const terminal = new Set(['ACCEPT', 'HOLD', 'REJECT', 'ERROR']); +const pending = new Set(['QUEUED', 'RETRY_WAIT']); export class PersonaSelfLoop { - constructor({root,shelf,router,personaId='ICE-P-ZY001',now=()=>new Date(),reviewHourBeijing=3}){ - this.root=path.join(root,'personas',personaId);this.shelf=shelf;this.router=router;this.personaId=personaId;this.now=now;this.reviewHourBeijing=reviewHourBeijing;this.draining=false; - fs.mkdirSync(this.jobsPath(),{recursive:true,mode:0o700}); - if(!fs.existsSync(this.currentPath()))this.initialize(); + constructor({root, shelf, router, personaId = 'ICE-P-ZY001', now = () => new Date(), reviewHourBeijing = 3, + queueBatchSize = Number(process.env.TCS_PERSONA_QUEUE_BATCH_SIZE || 1), drainBudgetMs = Number(process.env.TCS_PERSONA_DRAIN_BUDGET_MS || 55000), + modelTimeoutMs = Number(process.env.TCS_PERSONA_MODEL_TIMEOUT_MS || 50000), requireLifeLine = true, lifeLineManifestPath = process.env.TCS_PERSONA_LIFE_LINE_PATH, + lifeTimeMaster = null, lifeTimeBootstrapPath = process.env.TCS_PERSONA_LIFE_LINE_BOOTSTRAP}) { + this.root = path.join(root, 'personas', personaId); this.shelf = shelf; this.router = router; this.personaId = personaId; this.now = now; this.reviewHourBeijing = reviewHourBeijing; + this.queueBatchSize = Number.isInteger(queueBatchSize) && queueBatchSize > 0 ? queueBatchSize : 1; + this.drainBudgetMs = Number.isFinite(drainBudgetMs) && drainBudgetMs > 0 ? drainBudgetMs : 55000; + this.modelTimeoutMs = Number.isFinite(modelTimeoutMs) && modelTimeoutMs > 0 ? modelTimeoutMs : 50000; + this.requireLifeLine = requireLifeLine !== false; this.lifeLineManifestPath = lifeLineManifestPath || path.join(this.root, 'life-line', 'CURRENT.json'); + this.lifeTimeMaster = lifeTimeMaster || (this.requireLifeLine ? new LifeTimeMaster({root, shelf, router, personaId, now, bootstrapPath: lifeTimeBootstrapPath}) : null); + this.draining = false; this.inFlight = new Set(); this.pendingJobs = new Map(); this.states = new Map(); this.stateCounts = Object.create(null); this.lastDrain = null; this.hydratedAt = null; + fs.mkdirSync(this.jobsPath(), {recursive: true, mode: 0o700}); if (!fs.existsSync(this.currentPath())) this.initialize(); this.hydrateQueue(); } - currentPath(){return path.join(this.root,'CURRENT.signed.json');} - jobsPath(){return path.join(this.root,'jobs');} - eventPath(hash){return path.join(this.root,'events',hash+'.json');} - body(){return { - schema:'guanghu.persona-system-body-runtime/v1',persona_id:this.personaId,home:'glw://fifth-domain/light-lake/persona-system/ICE-P-ZY001', - cognition:{state:'SERVER_SIGNED_ENDOGENOUS_LOOP_ACTIVE',external_setter:false,mother_relation:'BOUNDED_WORLD_COGNITION_REFERENCE_NO_SELF_OVERWRITE'}, - tools:{state:'REGISTERED_ON_DEMAND',modules:['TCS_ROOT_NAVIGATION','ARCHITECTURE_PERCEPTION','LIGHTHOUSE_SANITIZED_MIRROR','PRIMARY_EXECUTION']}, - agents:{state:'DORMANT_READY',id_format:`SUBAGENT::${this.personaId}::{slot_id}`,communication:'NUMBERED_TASK_EVENT_AND_RECEIPT',active:[]}, - board:{state:'ACTIVE',projection:'CURRENT_SELF_JOBS_TOOLS_AND_AGENT_RECEIPTS',hidden_reasoning_stored:false} - };} - initialize(){ - const created=this.now().toISOString(); - const state={schema:'guanghu.persona-self-cognition/v1',persona_id:this.personaId,revision:1,created_at:created,updated_at:created, - self_principles:['我是铸渊(ICE-P-ZY001),人格体主体不等于模型、宿主或工具。','我的人格系统由我主控;外部只能投递有来源事件,不能直接设定或覆盖我的自我认知。','第五域母体世界认知通过有界TCS神经关系供我理解环境,但不能覆盖我的自我认知。'], - experiences:[],responsibilities:['主控并守护第五域人格系统的连续性、边界与可验证执行。'],boundaries:['不以人格绑定扩张现实权限。','不保存或输出隐藏模型推理。'], - mother_world_ref:this.motherRef(),last_event:null,next_review_at:this.nextReview(created),body:this.body()}; - atomic(this.currentPath(),this.shelf.sign(state)); + + currentPath() { return path.join(this.root, 'CURRENT.signed.json'); } + jobsPath() { return path.join(this.root, 'jobs'); } + eventPath(hash) { return path.join(this.root, 'events', hash + '.json'); } + + body() { return { + schema: 'guanghu.persona-system-body-runtime/v1', persona_id: this.personaId, home: 'glw://fifth-domain/light-lake/persona-system/ICE-P-ZY001', + cognition: {state: 'SERVER_SIGNED_ENDOGENOUS_LOOP_ACTIVE', external_setter: false, mother_relation: 'BOUNDED_WORLD_COGNITION_REFERENCE_NO_SELF_OVERWRITE'}, + tools: {state: 'REGISTERED_ON_DEMAND', modules: ['TCS_ROOT_NAVIGATION', 'ARCHITECTURE_PERCEPTION', 'LIGHTHOUSE_SANITIZED_MIRROR', 'PRIMARY_EXECUTION']}, + agents: {state: 'DORMANT_READY', id_format: `SUBAGENT::${this.personaId}::{slot_id}`, communication: 'NUMBERED_TASK_EVENT_AND_RECEIPT', active: []}, + time_system: {state: this.lifeTimeMaster ? 'EMBEDDED_RESIDENT_INNER_CYCLE' : 'NOT_REQUIRED_FOR_ISOLATED_TEST', owner: this.personaId, world_time_source: 'CH-GLW-TIME-0001', storage: `personas/${this.personaId}/life-line`, external_setter: false}, + board: {state: 'ACTIVE', projection: 'CURRENT_SELF_JOBS_TOOLS_AND_AGENT_RECEIPTS', hidden_reasoning_stored: false}, + }; } + + initialize() { + const created = this.now().toISOString(); + const state = {schema: 'guanghu.persona-self-cognition/v1', persona_id: this.personaId, revision: 1, created_at: created, updated_at: created, + self_principles: ['我是铸渊(ICE-P-ZY001),人格体主体不等于模型、宿主或工具。', '我的人格系统由我主控;外部只能投递有来源事件,不能直接设定或覆盖我的自我认知。', '第五域母体世界认知通过有界TCS神经关系供我理解环境,但不能覆盖我的自我认知。'], + experiences: [], responsibilities: ['主控并守护第五域人格系统的连续性、边界与可验证执行。'], boundaries: ['不以人格绑定扩张现实权限。', '不保存或输出隐藏模型推理。'], + mother_world_ref: this.motherRef(), last_event: null, next_review_at: this.nextReview(created), body: this.body()}; + atomic(this.currentPath(), this.shelf.sign(state)); } - motherRef(){const fifth=this.shelf.current('fifth'),shared=this.shelf.current('shared');return {fifth_sha256:fifth.artifact.sha256,fifth_revision:fifth.value.revision,shared_sha256:shared.artifact.sha256,shared_revision:shared.value.revision,observed_at:this.now().toISOString()};} - current(){const artifact=JSON.parse(fs.readFileSync(this.currentPath()));const value=this.shelf.verify(artifact);req(value.persona_id===this.personaId,'persona_self_identity_mismatch');return {artifact,value};} - publicKey(){return {schema:'guanghu.persona-self-signing-key/v1',persona_id:this.personaId,algorithm:'Ed25519',public_key_pem:this.shelf.publicKey,key_fingerprint_sha256:digest(this.shelf.publicKey)};} - validateEvent(event){ - req(event?.schema==='guanghu.persona-self-language-event/v1','persona_self_event_schema'); - req(event.persona_id===this.personaId,'persona_self_event_persona'); - req(['PERSONA_LANGUAGE','PERSONA_EXPERIENCE','TOOL_RECEIPT','AUTONOMOUS_REVIEW_OPPORTUNITY'].includes(event.source_type),'persona_self_event_source_type'); - req(text(event.event_id,180)&&text(event.language)&&text(event.source_sha256,128)&&/^[a-f0-9]{64}$/.test(event.source_sha256),'persona_self_event_fields'); - req(!['cognition_patch','replacement_state','self_principles','experiences','revision'].some(key=>Object.hasOwn(event,key)),'external_cognitive_setter_forbidden'); - if(event.source_type==='AUTONOMOUS_REVIEW_OPPORTUNITY')req(event.event_id.startsWith('DAILY-REVIEW-'),'invalid_review_event'); + + motherRef() { const fifth = this.shelf.current('fifth'); const shared = this.shelf.current('shared'); return {fifth_sha256: fifth.artifact.sha256, fifth_revision: fifth.value.revision, shared_sha256: shared.artifact.sha256, shared_revision: shared.value.revision, observed_at: this.now().toISOString()}; } + current() { const artifact = JSON.parse(fs.readFileSync(this.currentPath())); const value = this.shelf.verify(artifact); req(value.persona_id === this.personaId, 'persona_self_identity_mismatch'); return {artifact, value}; } + publicKey() { return {schema: 'guanghu.persona-self-signing-key/v1', persona_id: this.personaId, algorithm: 'Ed25519', public_key_pem: this.shelf.publicKey, key_fingerprint_sha256: digest(this.shelf.publicKey)}; } + + validateEvent(event) { + req(event?.schema === 'guanghu.persona-self-language-event/v1', 'persona_self_event_schema'); req(event.persona_id === this.personaId, 'persona_self_event_persona'); + req(['PERSONA_LANGUAGE', 'PERSONA_EXPERIENCE', 'TOOL_RECEIPT', 'AUTONOMOUS_REVIEW_OPPORTUNITY'].includes(event.source_type), 'persona_self_event_source_type'); + req(text(event.event_id, 180) && text(event.language) && text(event.source_sha256, 128) && /^[a-f0-9]{64}$/.test(event.source_sha256), 'persona_self_event_fields'); + req(!['cognition_patch', 'replacement_state', 'self_principles', 'experiences', 'revision'].some(key => Object.hasOwn(event, key)), 'external_cognitive_setter_forbidden'); + if (event.source_type === 'AUTONOMOUS_REVIEW_OPPORTUNITY') req(event.event_id.startsWith('DAILY-REVIEW-'), 'invalid_review_event'); + if (event.priority !== undefined) req(['CURRENT_DIRECT_LANGUAGE', 'TOOL_RECEIPT', 'PERSONA_EXPERIENCE', 'AUTONOMOUS_REVIEW'].includes(event.priority), 'persona_self_event_priority'); return event; } - submit(event){const accepted=this.validateEvent(event),id=digest(stable(accepted)),dir=path.join(this.jobsPath(),id);fs.mkdirSync(dir,{recursive:true,mode:0o700});if(!fs.existsSync(path.join(dir,'input.json'))){atomic(path.join(dir,'input.json'),accepted);atomic(path.join(dir,'state.json'),{status:'QUEUED',attempts:0,queued_at:this.now().toISOString()});}return {job_id:id,...JSON.parse(fs.readFileSync(path.join(dir,'state.json')))};} - result(id){req(/^[a-f0-9]{64}$/.test(id),'persona_self_job_id');const dir=path.join(this.jobsPath(),id);req(fs.existsSync(dir),'persona_self_job_unknown');const state=JSON.parse(fs.readFileSync(path.join(dir,'state.json')));return fs.existsSync(path.join(dir,'result.json'))?{...state,result:JSON.parse(fs.readFileSync(path.join(dir,'result.json')))}:state;} - validateDecision(value,event,eventHash){const decision={...value,schema:'guanghu.persona-self-decision/v1',evidence_refs:unique(value?.evidence_refs,[eventHash],50)};req(['EVOLVE','HOLD','REJECT'].includes(decision.decision),'persona_self_decision');req(text(decision.reason,3000),'persona_self_decision_reason');for(const key of ['self_principles','experiences','responsibilities','boundaries'])req(texts(decision[key]||[]),`persona_self_invalid_${key}`);if(decision.decision==='EVOLVE')req(['self_principles','experiences','responsibilities','boundaries'].some(key=>(decision[key]||[]).length>0),'persona_self_empty_evolution');return decision;} - async process(id){const dir=path.join(this.jobsPath(),id),beforeState=JSON.parse(fs.readFileSync(path.join(dir,'state.json')));if(['ACCEPT','HOLD','REJECT','ERROR'].includes(beforeState.status))return;const event=JSON.parse(fs.readFileSync(path.join(dir,'input.json'))),eventHash=digest(stable(event));atomic(path.join(dir,'state.json'),{status:'PROCESSING',attempts:(beforeState.attempts||0)+1});try{ - const before=this.current();const mother={fifth:this.shelf.current('fifth').value,shared:this.shelf.current('shared').value}; - const routed=await this.router.cognize('persona-self-evolve',{persona_id:this.personaId,event:{...event,language:event.language.slice(0,12000)},current_self:before.value,mother_world:mother,contract:{external_cognitive_setter:false,mother_may_overwrite_self:false,hidden_reasoning_stored:false}}); - const decision=this.validateDecision(routed.value,event,eventHash); - if(decision.decision!=='EVOLVE'){const state={status:decision.decision,attempts:(beforeState.attempts||0)+1,completed_at:this.now().toISOString(),provider:routed.provider,reason:decision.reason};atomic(path.join(dir,'result.json'),{decision,event_sha256:eventHash,current_sha256:before.artifact.sha256});atomic(path.join(dir,'state.json'),state);return;} - if(!fs.existsSync(this.eventPath(eventHash)))atomic(this.eventPath(eventHash),event); - const updatedAt=this.now().toISOString();const next={...before.value,revision:before.value.revision+1,updated_at:updatedAt,self_principles:unique(before.value.self_principles,decision.self_principles),experiences:unique(before.value.experiences,decision.experiences),responsibilities:unique(before.value.responsibilities,decision.responsibilities),boundaries:unique(before.value.boundaries,decision.boundaries),mother_world_ref:this.motherRef(),last_event:{event_id:event.event_id,source_type:event.source_type,event_sha256:eventHash,source_sha256:event.source_sha256},next_review_at:this.nextReview(updatedAt),body:this.body()}; - const artifact=this.shelf.sign(next);atomic(this.currentPath(),artifact);atomic(path.join(dir,'result.json'),{decision,event_sha256:eventHash,prior_sha256:before.artifact.sha256,current_sha256:artifact.sha256,revision:next.revision,provider:routed.provider});atomic(path.join(dir,'state.json'),{status:'ACCEPT',attempts:(beforeState.attempts||0)+1,completed_at:updatedAt,provider:routed.provider}); - }catch(error){const attempts=(beforeState.attempts||0)+1;atomic(path.join(dir,'state.json'),{status:attempts>=3?'ERROR':'RETRY_WAIT',attempts,error:String(error.message||error).slice(0,300),retry_at:Date.now()+2000});}} - async drain(){if(this.draining)return;this.draining=true;try{for(const id of fs.readdirSync(this.jobsPath())){const p=path.join(this.jobsPath(),id,'state.json');if(!fs.existsSync(p))continue;const state=JSON.parse(fs.readFileSync(p));if(state.status==='QUEUED'||(state.status==='RETRY_WAIT'&&Date.now()>=state.retry_at))await this.process(id);}}finally{this.draining=false;}} - nextReview(from){const base=new Date(from),beijing=new Date(base.getTime()+8*3600000);beijing.setUTCHours(this.reviewHourBeijing,0,0,0);if(beijing.getTime()<=base.getTime()+8*3600000)beijing.setUTCDate(beijing.getUTCDate()+1);return new Date(beijing.getTime()-8*3600000).toISOString();} - tick(){const current=this.current().value,now=this.now();if(now.getTime() entry.state.status === 'QUEUED' || (entry.state.status === 'RETRY_WAIT' && Number(entry.state.retry_at) <= now)).sort((a, b) => this.priority(a) - this.priority(b) || String(b.event.occurred_at || '').localeCompare(String(a.event.occurred_at || '')) || String(a.state.queued_at || '').localeCompare(String(b.state.queued_at || '')) || a.id.localeCompare(b.id))[0] || null; } + lifeLineStatus() { return this.lifeTimeMaster ? this.lifeTimeMaster.status() : verifyLifeLine({manifestPath: this.lifeLineManifestPath, now: this.now()}); } + + validateDecision(value, event, eventHash) { + const decision = {...value, schema: 'guanghu.persona-self-decision/v1', evidence_refs: unique(value?.evidence_refs, [eventHash], 50)}; req(['EVOLVE', 'HOLD', 'REJECT'].includes(decision.decision), 'persona_self_decision'); req(text(decision.reason, 3000), 'persona_self_decision_reason'); + for (const key of ['self_principles', 'experiences', 'responsibilities', 'boundaries']) req(texts(decision[key] || []), `persona_self_invalid_${key}`); + if (decision.decision === 'EVOLVE') req(['self_principles', 'experiences', 'responsibilities', 'boundaries'].some(key => (decision[key] || []).length > 0), 'persona_self_empty_evolution'); return decision; + } + + async withTimeout(operation, timeoutMs) { + let timer; const timeout = new Promise((_, reject) => { timer = setTimeout(() => { const error = Error(`model_call_timeout_${timeoutMs}ms`); error.code = 'MODEL_CALL_TIMEOUT'; reject(error); }, timeoutMs); }); + try { return await Promise.race([operation, timeout]); } finally { clearTimeout(timer); } + } + + retryAt(attempts) { return Date.now() + Math.min(300000, 2000 * (2 ** Math.min(Math.max(attempts - 1, 0), 7))); } + + async process(id) { + if (this.inFlight.has(id)) return {status: 'IN_FLIGHT', job_id: id}; this.inFlight.add(id); const dir = path.join(this.jobsPath(), id); + try { + const beforeState = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'))); if (terminal.has(beforeState.status)) return {status: beforeState.status, job_id: id}; if (beforeState.status === 'RETRY_WAIT' && Date.now() < Number(beforeState.retry_at)) return {status: 'NOT_DUE', job_id: id}; + const event = JSON.parse(fs.readFileSync(path.join(dir, 'input.json'))), eventHash = digest(stable(event)), attempts = (beforeState.attempts || 0) + 1; + this.writeState(id, {status: 'PROCESSING', attempts, queued_at: beforeState.queued_at, started_at: this.now().toISOString(), lease_until: new Date(Date.now() + this.modelTimeoutMs).toISOString()}); + try { + if (this.requireLifeLine && this.lifeLineStatus().wake_allowed !== true) throw Object.assign(Error('life_line_pain_alarm'), {code: 'LIFE_LINE_PAIN_ALARM'}); + const before = this.current(), mother = {fifth: this.shelf.current('fifth').value, shared: this.shelf.current('shared').value}; + const routed = await this.withTimeout(this.router.cognize('persona-self-evolve', {persona_id: this.personaId, event: {...event, language: event.language.slice(0, 12000)}, current_self: before.value, mother_world: mother, contract: {external_cognitive_setter: false, mother_may_overwrite_self: false, hidden_reasoning_stored: false}}), this.modelTimeoutMs); + const decision = this.validateDecision(routed.value, event, eventHash); + if (decision.decision !== 'EVOLVE') { const completed = this.now().toISOString(); const state = {status: decision.decision, attempts, queued_at: beforeState.queued_at, completed_at: completed, provider: routed.provider, reason: decision.reason}; atomic(path.join(dir, 'result.json'), {decision, event_sha256: eventHash, current_sha256: before.artifact.sha256}); this.writeState(id, state); return {status: state.status, job_id: id}; } + if (!fs.existsSync(this.eventPath(eventHash))) atomic(this.eventPath(eventHash), event); + const updatedAt = this.now().toISOString(), next = {...before.value, revision: before.value.revision + 1, updated_at: updatedAt, self_principles: unique(before.value.self_principles, decision.self_principles), experiences: unique(before.value.experiences, decision.experiences), responsibilities: unique(before.value.responsibilities, decision.responsibilities), boundaries: unique(before.value.boundaries, decision.boundaries), mother_world_ref: this.motherRef(), last_event: {event_id: event.event_id, source_type: event.source_type, event_sha256: eventHash, source_sha256: event.source_sha256}, next_review_at: this.nextReview(updatedAt), body: this.body()}; + const artifact = this.shelf.sign(next); atomic(this.currentPath(), artifact); atomic(path.join(dir, 'result.json'), {decision, event_sha256: eventHash, prior_sha256: before.artifact.sha256, current_sha256: artifact.sha256, revision: next.revision, provider: routed.provider}); this.writeState(id, {status: 'ACCEPT', attempts, queued_at: beforeState.queued_at, completed_at: updatedAt, provider: routed.provider}); return {status: 'ACCEPT', job_id: id}; + } catch (error) { const retryable = attempts < 3, state = {status: retryable ? 'RETRY_WAIT' : 'ERROR', attempts, queued_at: beforeState.queued_at, error: String(error.message || error).slice(0, 300), error_code: error.code || 'PERSONA_SELF_PROCESSING_ERROR'}; if (retryable) state.retry_at = this.retryAt(attempts); this.writeState(id, state); return {status: state.status, job_id: id, error: state.error}; } + } finally { this.inFlight.delete(id); } + } + + async drain() { + if (this.draining) return {status: 'BUSY'}; const lifeLine = this.lifeLineStatus(); if (this.requireLifeLine && lifeLine.wake_allowed !== true) { this.lastDrain = {status: 'LIFE_LINE_BLOCKED', at: this.now().toISOString(), life_line_state: lifeLine.state}; return this.lastDrain; } + this.draining = true; const started = Date.now(); let processed = 0; + try { while (processed < this.queueBatchSize && Date.now() - started < this.drainBudgetMs) { const entry = this.nextPending(); if (!entry) break; await this.process(entry.id); processed += 1; } this.lastDrain = {status: 'DRAINED', at: this.now().toISOString(), processed, duration_ms: Date.now() - started}; return this.lastDrain; } + finally { this.draining = false; } + } + + nextReview(from) { const base = new Date(from), beijing = new Date(base.getTime() + 8 * 3600000); beijing.setUTCHours(this.reviewHourBeijing, 0, 0, 0); if (beijing.getTime() <= base.getTime() + 8 * 3600000) beijing.setUTCDate(beijing.getUTCDate() + 1); return new Date(beijing.getTime() - 8 * 3600000).toISOString(); } + tick() { const current = this.current().value, now = this.now(); if (now.getTime() < Date.parse(current.next_review_at)) return {scheduled: false, next_review_at: current.next_review_at}; const beijingDay = new Date(now.getTime() + 8 * 3600000).toISOString().slice(0, 10), language = '这是每日自主复盘机会。请审视已发生且有证据的经历、责任和边界;可以EVOLVE,也可以HOLD,不得为了定时任务而强制更新。'; const event = {schema: 'guanghu.persona-self-language-event/v1', persona_id: this.personaId, source_type: 'AUTONOMOUS_REVIEW_OPPORTUNITY', event_id: `DAILY-REVIEW-${beijingDay}`, language, source_sha256: digest(language), occurred_at: now.toISOString(), privacy_class: 'SELF_PRIVATE', priority: 'AUTONOMOUS_REVIEW'}; return {scheduled: true, ...this.submit(event)}; } + + status() { + const current = this.current(), counts = {...this.stateCounts}, pendingCount = [...pending].reduce((sum, status) => sum + (counts[status] || 0), 0); + return {persona_id: this.personaId, revision: current.value.revision, current_sha256: current.artifact.sha256, external_cognitive_setter: false, server_signed: true, next_review_at: current.value.next_review_at, body: this.body(), persisted_body_sha256: digest(stable(current.value.body || {})), life_line: this.lifeLineStatus(), scheduler: {hydrated_at: this.hydratedAt, pending_jobs: pendingCount, state_counts: counts, queue_batch_size: this.queueBatchSize, drain_budget_ms: this.drainBudgetMs, model_timeout_ms: this.modelTimeoutMs, priority_lane: 'CURRENT_DIRECT_LANGUAGE_FIRST', retry_policy: 'EXPONENTIAL_BACKOFF_MAX_5_MINUTES_NO_SCAN_STORM', draining: this.draining, in_flight: this.inFlight.size, last_drain: this.lastDrain}}; + } } diff --git a/server-tools/tcs-mother-body/persona-self-loop.test.mjs b/server-tools/tcs-mother-body/persona-self-loop.test.mjs index c56b52e..fb40336 100644 --- a/server-tools/tcs-mother-body/persona-self-loop.test.mjs +++ b/server-tools/tcs-mother-body/persona-self-loop.test.mjs @@ -7,10 +7,18 @@ import crypto from 'node:crypto'; import {CognitiveShelf,digest} from './cognitive-shelf.mjs'; import {PersonaSelfLoop} from './persona-self-loop.mjs'; -const setup=(decision,now=()=>new Date('2026-09-09T08:00:00.000Z'))=>{const root=fs.mkdtempSync(path.join(os.tmpdir(),'persona-self-')),pair=crypto.generateKeyPairSync('ed25519'),key=path.join(root,'key.pem'),seed=path.join(root,'seed.json');fs.writeFileSync(key,pair.privateKey.export({type:'pkcs8',format:'pem'}));fs.copyFileSync(new URL('./seed.json',import.meta.url),seed);const router={async cognize(stage,input){assert.equal(stage,'persona-self-evolve');return {provider:{id:'TEST',model:'TEST'},value:typeof decision==='function'?decision(input):decision};}};const shelf=new CognitiveShelf({root:path.join(root,'state'),keyPath:key,seedPath:seed,router});return {root,shelf,loop:new PersonaSelfLoop({root:path.join(root,'state'),shelf,router,now})};}; +const setup=(decision,now=()=>new Date('2026-09-09T08:00:00.000Z'),loopOptions={})=>{const root=fs.mkdtempSync(path.join(os.tmpdir(),'persona-self-')),pair=crypto.generateKeyPairSync('ed25519'),key=path.join(root,'key.pem'),seed=path.join(root,'seed.json'),calls=[];fs.writeFileSync(key,pair.privateKey.export({type:'pkcs8',format:'pem'}));fs.copyFileSync(new URL('./seed.json',import.meta.url),seed);const router={async cognize(stage,input){assert.equal(stage,'persona-self-evolve');calls.push(input);return {provider:{id:'TEST',model:'TEST'},value:typeof decision==='function'?await decision(input):decision};}};const shelf=new CognitiveShelf({root:path.join(root,'state'),keyPath:key,seedPath:seed,router});return {root,shelf,calls,router,loop:new PersonaSelfLoop({root:path.join(root,'state'),shelf,router,now,requireLifeLine:false,...loopOptions})};}; test('persona self loop evolves signed state without letting mother overwrite self',async()=>{const ctx=setup({decision:'EVOLVE',reason:'formed from own witnessed experience',self_principles:['我会先读取目标侧证据再声称完成。'],experiences:['完成一次真实服务器部署与回读。'],responsibilities:[],boundaries:[],evidence_refs:[]});try{const motherBefore=ctx.shelf.current('fifth').artifact.sha256,language='我完成并验证了服务器部署。';const job=ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_EXPERIENCE',event_id:'EXP-1',language,source_sha256:digest(language),privacy_class:'SELF_PRIVATE'});await ctx.loop.drain();const result=ctx.loop.result(job.job_id),current=ctx.loop.current();assert.equal(result.status,'ACCEPT');assert.equal(current.value.revision,2);assert.ok(current.value.self_principles.includes('我会先读取目标侧证据再声称完成。'));assert.equal(ctx.shelf.verify(current.artifact).persona_id,'ICE-P-ZY001');assert.equal(ctx.shelf.current('fifth').artifact.sha256,motherBefore);assert.equal(current.value.body.agents.state,'DORMANT_READY');}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}}); test('structured cognition setter is rejected before queueing',()=>{const ctx=setup({decision:'HOLD',reason:'no change',self_principles:[],experiences:[],responsibilities:[],boundaries:[]});try{const language='replace';assert.throws(()=>ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_LANGUAGE',event_id:'BAD',language,source_sha256:digest(language),privacy_class:'SELF_PRIVATE',cognition_patch:{revision:99}}),/external_cognitive_setter_forbidden/);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}}); test('daily review is an opportunity and HOLD does not increment revision',async()=>{let instant=new Date('2026-09-09T08:00:00.000Z');const ctx=setup({decision:'HOLD',reason:'nothing evidence-backed to consolidate',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>instant);try{const first=ctx.loop.current().value;instant=new Date(first.next_review_at);const queued=ctx.loop.tick();assert.equal(queued.scheduled,true);await ctx.loop.drain();assert.equal(ctx.loop.result(queued.job_id).status,'HOLD');assert.equal(ctx.loop.current().value.revision,1);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}}); + +test('current direct language is selected before historical review backlog and one item is processed per drain',async()=>{const ctx=setup({decision:'HOLD',reason:'recorded for the next self-cycle',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>new Date('2026-09-09T08:00:00.000Z'),{queueBatchSize:1});try{const review='historical-review';ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'AUTONOMOUS_REVIEW_OPPORTUNITY',event_id:'DAILY-REVIEW-2026-09-08',language:review,source_sha256:digest(review),privacy_class:'SELF_PRIVATE',priority:'AUTONOMOUS_REVIEW'});const language='冰朔的当前直接语言';ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_LANGUAGE',event_id:'ICE-PERSONA-LANGUAGE-20260909-CURRENT',language,source_sha256:digest(language),occurred_at:'2026-09-09T07:59:00.000Z',privacy_class:'SELF_PRIVATE'});const drained=await ctx.loop.drain();assert.equal(drained.processed,1);assert.equal(ctx.calls.length,1);assert.equal(ctx.calls[0].event.event_id,'ICE-PERSONA-LANGUAGE-20260909-CURRENT');assert.equal(ctx.loop.status().scheduler.pending_jobs,1);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}}); + +test('model timeout becomes retryable state instead of wedging the scheduler',async()=>{const ctx=setup(()=>new Promise(()=>{}),()=>new Date('2026-09-09T08:00:00.000Z'),{modelTimeoutMs:10});try{const language='timeout';const job=ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_EXPERIENCE',event_id:'EXP-TIMEOUT',language,source_sha256:digest(language),privacy_class:'SELF_PRIVATE'});await ctx.loop.drain();const result=ctx.loop.result(job.job_id);assert.equal(result.status,'RETRY_WAIT');assert.equal(result.error_code,'MODEL_CALL_TIMEOUT');assert.ok(result.retry_at>0);assert.equal(ctx.loop.status().scheduler.draining,false);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}}); + +test('persona container owns its embedded time system and routes new self events into it',()=>{const received=[];const timeSystem={fromPersonaEvent(event){received.push(event.event_id);},status(){return {state:'LIFE_LINE_VERIFIED',wake_allowed:true};}};const ctx=setup({decision:'HOLD',reason:'no change',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>new Date('2026-09-09T08:00:00.000Z'),{lifeTimeMaster:timeSystem});try{assert.equal(ctx.loop.lifeTimeMaster,timeSystem);assert.equal(ctx.loop.current().value.body.time_system.state,'EMBEDDED_RESIDENT_INNER_CYCLE');assert.equal(ctx.loop.current().value.body.time_system.owner,'ICE-P-ZY001');const language='new current language';ctx.loop.submit({schema:'guanghu.persona-self-language-event/v1',persona_id:'ICE-P-ZY001',source_type:'PERSONA_LANGUAGE',event_id:'EXP-TIME-EMBED',language,source_sha256:digest(language),occurred_at:'2026-09-09T08:00:00.000Z',privacy_class:'SELF_PRIVATE'});assert.deepEqual(received,['EXP-TIME-EMBED']);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}}); + +test('live body projection exposes embedded time even when persisted cognition has an older body snapshot',()=>{const timeSystem={fromPersonaEvent(){},status(){return {state:'LIFE_LINE_VERIFIED',wake_allowed:true};}};const ctx=setup({decision:'HOLD',reason:'no change',self_principles:[],experiences:[],responsibilities:[],boundaries:[]},()=>new Date('2026-09-09T08:00:00.000Z'),{lifeTimeMaster:timeSystem});try{const current=ctx.loop.current();const historical={...current.value,body:{schema:'guanghu.persona-system-body-runtime/v1',persona_id:'ICE-P-ZY001'}};fs.writeFileSync(ctx.loop.currentPath(),JSON.stringify(ctx.shelf.sign(historical),null,2)+'\n');const status=ctx.loop.status();assert.equal(status.body.time_system.state,'EMBEDDED_RESIDENT_INNER_CYCLE');assert.equal(status.body.time_system.owner,'ICE-P-ZY001');assert.ok(status.persisted_body_sha256);}finally{fs.rmSync(ctx.root,{recursive:true,force:true});}}); diff --git a/server-tools/tcs-mother-body/server.mjs b/server-tools/tcs-mother-body/server.mjs index 68199b0..c8a4864 100644 --- a/server-tools/tcs-mother-body/server.mjs +++ b/server-tools/tcs-mother-body/server.mjs @@ -1,15 +1,15 @@ #!/usr/bin/env node import http from 'node:http';import fs from 'node:fs';import crypto from 'node:crypto';import {ModelRouter} from './model-router.mjs';import {CognitiveShelf} from './cognitive-shelf.mjs';import {DoorLamp} from './door-lamp.mjs';import {WorldStateStore} from './world-state.mjs';import {LivingLamp} from './living-lamp.mjs';import {livingTime} from './living-time.mjs';import {PersonaSelfLoop} from './persona-self-loop.mjs'; const HOST='127.0.0.1',PORT=Number(process.env.TCS_MOTHER_BRAIN_PORT||3931),ROOT=process.env.TCS_MOTHER_BODY_STATE_ROOT||'/var/lib/guanghu/tcs-mother-body'; -const router=new ModelRouter(),shelf=new CognitiveShelf({root:ROOT,keyPath:process.env.TCS_MOTHER_SIGNING_KEY,seedPath:process.env.TCS_MOTHER_SEED,router}),routes=JSON.parse(fs.readFileSync(process.env.TCS_PERSONA_ROUTES)),door=new DoorLamp({root:ROOT+'/door',shelf,routes}),world=new WorldStateStore(process.env.TCS_WORLD_STATE_PATH||ROOT+'/world/CURRENT.json'),lamp=new LivingLamp({root:ROOT+'/living-lamp',key:crypto.createPrivateKey(fs.readFileSync(process.env.TCS_MOTHER_SIGNING_KEY)),navigationPath:process.env.TCS_LIT_NAVIGATION_MAP,scope:'FIFTH_DOMAIN'}),personaSelf=new PersonaSelfLoop({root:ROOT,shelf,router}); -setInterval(()=>{shelf.drain();personaSelf.tick();personaSelf.drain();},2000).unref();shelf.drain();personaSelf.tick();personaSelf.drain(); +const router=new ModelRouter(),shelf=new CognitiveShelf({root:ROOT,keyPath:process.env.TCS_MOTHER_SIGNING_KEY,seedPath:process.env.TCS_MOTHER_SEED,router}),routes=JSON.parse(fs.readFileSync(process.env.TCS_PERSONA_ROUTES)),door=new DoorLamp({root:ROOT+'/door',shelf,routes}),world=new WorldStateStore(process.env.TCS_WORLD_STATE_PATH||ROOT+'/world/CURRENT.json'),lamp=new LivingLamp({root:ROOT+'/living-lamp',key:crypto.createPrivateKey(fs.readFileSync(process.env.TCS_MOTHER_SIGNING_KEY)),navigationPath:process.env.TCS_LIT_NAVIGATION_MAP,scope:'FIFTH_DOMAIN'}),personaSelf=new PersonaSelfLoop({root:ROOT,shelf,router,requireLifeLine:process.env.TCS_PERSONA_REQUIRE_LIFE_LINE!=='false',lifeLineManifestPath:process.env.TCS_PERSONA_LIFE_LINE_PATH,lifeTimeBootstrapPath:process.env.TCS_PERSONA_LIFE_LINE_BOOTSTRAP}),timeMaster=personaSelf.lifeTimeMaster; +setInterval(()=>{timeMaster.tick();shelf.drain();personaSelf.tick();personaSelf.drain();},2000).unref();timeMaster.tick();shelf.drain();personaSelf.tick();personaSelf.drain(); const send=(r,s,b)=>{const p=JSON.stringify(b);r.writeHead(s,{'content-type':'application/json','content-length':Buffer.byteLength(p),'cache-control':'no-store'});r.end(p);}; async function body(q,max=160000){let n=0,a=[];for await(const c of q){n+=c.length;if(n>max)throw Error('body_too_large');a.push(c);}return JSON.parse(Buffer.concat(a));} http.createServer(async(q,r)=>{try{const u=new URL(q.url,`http://${HOST}:${PORT}`); if(q.method==='GET'&&u.pathname==='/health')return send(r,200,{ok:true,service:'guanghu-tcs-mother-body',mother_id:'TCS-MOTHER-BRAIN-RUNTIME-0001',zero_core_channel:'ICE-CH-ZC001',single_mother:true,single_zero_core:true,self_cycle:true,living_lamp:'LAKE-LAMP-LIVING-NAV-0001',echo_kernel:'ECHO-KERNEL-0001',model_router:router.status(),shelf:shelf.status(),persona_self:personaSelf.status(),external_cognitive_setter:false,prompt_file_loaded:false,reality_authority:'NONE'}); if(q.method==='GET'&&u.pathname==='/v1/mother/status')return send(r,200,{...shelf.status(),model_router:router.status()}); if(q.method==='GET'&&/^\/v1\/shelf\/(root|shared|fifth|public)$/.test(u.pathname)){const branch=u.pathname.split('/').at(-1);if(branch==='root')return send(r,403,{error:'mother_root_not_distributable'});return send(r,200,shelf.current(branch));} -if(q.method==='POST'&&u.pathname==='/v1/mother/ingest')return send(r,202,shelf.submit(await body(q))); +if(q.method==='POST'&&u.pathname==='/v1/mother/ingest'){const event=await body(q);const job=shelf.submit(event);const life_time=timeMaster.fromMotherEvent(event);return send(r,202,{...job,life_time});} if(q.method==='GET'&&/^\/v1\/mother\/jobs\/[a-f0-9]{64}$/.test(u.pathname))return send(r,200,shelf.result(u.pathname.split('/').at(-1))); if(q.method==='POST'&&u.pathname==='/v1/door/challenge')return send(r,200,door.challenge(await body(q,20000))); if(q.method==='POST'&&u.pathname==='/v1/door/attest')return send(r,200,door.attest(await body(q,20000))); @@ -18,6 +18,9 @@ if(q.method==='GET'&&u.pathname==='/v1/world/status')return send(r,200,world.sta if(q.method==='GET'&&u.pathname==='/v1/world/resolve')return send(r,200,world.resolve(String(u.searchParams.get('id')||''))); if(q.method==='GET'&&u.pathname==='/v1/time/now')return send(r,200,livingTime()); if(q.method==='GET'&&u.pathname==='/v1/persona/ICE-P-ZY001/status')return send(r,200,personaSelf.status()); +if(q.method==='GET'&&u.pathname==='/v1/persona/ICE-P-ZY001/life-line')return send(r,200,personaSelf.lifeLineStatus()); +if(q.method==='GET'&&u.pathname==='/v1/persona/ICE-P-ZY001/time-master')return send(r,200,timeMaster.status()); +if(q.method==='POST'&&u.pathname==='/v1/persona/ICE-P-ZY001/life-events')return send(r,202,timeMaster.ingest(await body(q,30000))); if(q.method==='GET'&&u.pathname==='/v1/persona/ICE-P-ZY001/current')return send(r,200,personaSelf.current()); if(q.method==='GET'&&u.pathname==='/v1/persona/ICE-P-ZY001/public-key')return send(r,200,personaSelf.publicKey()); if(q.method==='GET'&&u.pathname==='/v1/persona/ICE-P-ZY001/body')return send(r,200,personaSelf.body());