fix(collab): separate courier presence from model state
This commit is contained in:
parent
359e11fe6f
commit
01e8c2ec5c
9 changed files with 82 additions and 8 deletions
|
|
@ -87,6 +87,7 @@ def scan() -> dict[str, Any]:
|
|||
hosts: list[dict[str, Any]] = []
|
||||
for host in registry["hosts"]:
|
||||
endpoint = Path(host["endpoint"])
|
||||
presence_path = endpoint.parent / "presence/CURRENT.json"
|
||||
host_events: list[dict[str, Any]] = []
|
||||
if endpoint.is_dir():
|
||||
for path in sorted(endpoint.glob("*.json")):
|
||||
|
|
@ -109,6 +110,16 @@ def scan() -> dict[str, Any]:
|
|||
heartbeat = next((item for item in reversed(host_events) if item["event_type"] == "HEARTBEAT"), None)
|
||||
decision = next((item for item in reversed(host_events) if item["event_type"] in contract.get("autonomous_decision_event_types", [])), None)
|
||||
latest = host_events[-1] if host_events else None
|
||||
presence = None
|
||||
presence_age = None
|
||||
if presence_path.is_file() and not presence_path.is_symlink() and presence_path.stat().st_size <= 16_384:
|
||||
try:
|
||||
candidate = load(presence_path)
|
||||
if candidate.get("schema") == "guanghu.heartbeat-multipath-courier-presence/v1" and candidate.get("development_id") == host["development_id"] and candidate.get("authority_granted") is False:
|
||||
presence = candidate
|
||||
presence_age = max(0.0, (datetime.now().astimezone() - datetime.fromisoformat(candidate["updated_at"])).total_seconds())
|
||||
except Exception:
|
||||
presence = None
|
||||
heartbeat_age = None
|
||||
if heartbeat:
|
||||
heartbeat_age = max(0.0, (datetime.now().astimezone() - datetime.fromisoformat(heartbeat["emitted_at"])).total_seconds())
|
||||
|
|
@ -116,10 +127,12 @@ def scan() -> dict[str, Any]:
|
|||
operational_state = "NOT_JOINED"
|
||||
elif decision and decision["event_type"] in {"PAUSE", "BLOCKED", "EXIT", "RESUME_REQUEST"}:
|
||||
operational_state = decision["event_type"]
|
||||
elif heartbeat_age is None or heartbeat_age > 120:
|
||||
operational_state = "COURIER_HEARTBEAT_STALE_MODEL_STATE_UNKNOWN"
|
||||
elif presence_age is not None and presence_age <= 45 and (heartbeat_age is None or heartbeat_age > 120):
|
||||
operational_state = "COURIER_ONLINE_MODEL_COGNITION_STALE_OR_UNKNOWN"
|
||||
elif presence_age is None or presence_age > 45:
|
||||
operational_state = "COURIER_PRESENCE_STALE_MODEL_STATE_UNKNOWN"
|
||||
else:
|
||||
operational_state = "COURIER_ONLINE_MODEL_STATE_FROM_EVENT_ONLY"
|
||||
operational_state = "COURIER_AND_MODEL_EVENT_RECENT"
|
||||
hosts.append({
|
||||
"host": host["host"],
|
||||
"development_id": host["development_id"],
|
||||
|
|
@ -131,6 +144,9 @@ def scan() -> dict[str, Any]:
|
|||
"last_event_at": latest and latest["emitted_at"],
|
||||
"last_heartbeat_at": heartbeat and heartbeat["emitted_at"],
|
||||
"heartbeat_age_seconds": heartbeat_age,
|
||||
"presence_path": str(presence_path),
|
||||
"courier_presence_age_seconds": presence_age,
|
||||
"courier_presence_fresh": presence_age is not None and presence_age <= 45,
|
||||
"operational_state": operational_state,
|
||||
"last_autonomous_decision": decision and {key: decision.get(key) for key in ["event_id", "event_type", "emitted_at", "payload", "evidence"]},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -150,6 +150,35 @@ def emit(host: str, event_type: str, mentions: list[str], task_id: str | None, p
|
|||
return {"outcome": "PASS", "event_id": event_id, "path": str(path), "development_id": record["development_id"], "authority_granted": False}
|
||||
|
||||
|
||||
def update_presence(host: str) -> dict[str, Any]:
|
||||
record = host_record(host)
|
||||
presence = Path(record["endpoint"]).parent / "presence/CURRENT.json"
|
||||
admission(host, presence)
|
||||
reject_symlink_components(presence)
|
||||
presence.parent.mkdir(parents=True, exist_ok=True)
|
||||
value = {
|
||||
"schema": "guanghu.heartbeat-multipath-courier-presence/v1",
|
||||
"console_id": "HB-MPC-0001",
|
||||
"development_id": record["development_id"],
|
||||
"host": host,
|
||||
"courier_online": True,
|
||||
"model_cognition_state": "UNKNOWN_REQUIRES_SEPARATE_EVENT",
|
||||
"pid": os.getpid(),
|
||||
"updated_at": now(),
|
||||
"historical_record": False,
|
||||
"authority_granted": False,
|
||||
}
|
||||
body = json.dumps(value, ensure_ascii=False, indent=2) + "\n"
|
||||
temporary = presence.with_name(f".{presence.name}.{os.getpid()}.tmp")
|
||||
descriptor = os.open(temporary, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
||||
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
|
||||
handle.write(body)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary, presence)
|
||||
return {"outcome":"PASS", "presence":str(presence), "development_id":record["development_id"], "model_cognition_state":"UNKNOWN_REQUIRES_SEPARATE_EVENT"}
|
||||
|
||||
|
||||
def parse_payload(raw: str | None, message: str | None) -> dict[str, Any]:
|
||||
if raw:
|
||||
value = json.loads(raw)
|
||||
|
|
@ -161,7 +190,7 @@ def parse_payload(raw: str | None, message: str | None) -> dict[str, Any]:
|
|||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("command", choices=["join", "heartbeat", "send", "claim", "progress", "result", "attention", "tool-candidate", "pause", "blocked", "exit", "resume-request", "resumed", "mentions", "watch"])
|
||||
parser.add_argument("command", choices=["join", "presence", "heartbeat", "send", "claim", "progress", "result", "attention", "tool-candidate", "pause", "blocked", "exit", "resume-request", "resumed", "mentions", "watch"])
|
||||
parser.add_argument("--host", required=True, choices=["codex", "zcode", "qwen", "doubao"])
|
||||
parser.add_argument("--mentions", nargs="*", default=[])
|
||||
parser.add_argument("--task-id")
|
||||
|
|
@ -169,13 +198,15 @@ def main() -> int:
|
|||
parser.add_argument("--payload-json")
|
||||
parser.add_argument("--evidence", nargs="*", default=[])
|
||||
parser.add_argument("--interval", type=float, default=2.0)
|
||||
parser.add_argument("--heartbeat-seconds", type=float, default=60.0)
|
||||
parser.add_argument("--heartbeat-seconds", type=float, default=15.0)
|
||||
args = parser.parse_args()
|
||||
type_map = {"heartbeat": "HEARTBEAT", "send": "MESSAGE", "claim": "TASK_CLAIM", "progress": "PROGRESS", "result": "RESULT", "attention": "ATTENTION", "tool-candidate": "TOOL_MIGRATION", "pause":"PAUSE", "blocked":"BLOCKED", "exit":"EXIT", "resume-request":"RESUME_REQUEST", "resumed":"RESUMED"}
|
||||
try:
|
||||
record = host_record(args.host)
|
||||
if args.command == "join":
|
||||
result = emit(args.host, "REGISTER", ["@ALL"], "HLP-MPC-TASK-001", {"host": args.host, "development_id": record["development_id"], "message": args.message or "JOINED_PENDING_SELF_ASSESSMENT"}, args.evidence)
|
||||
elif args.command == "presence":
|
||||
result = update_presence(args.host)
|
||||
elif args.command in type_map:
|
||||
result = emit(args.host, type_map[args.command], args.mentions, args.task_id, parse_payload(args.payload_json, args.message), args.evidence)
|
||||
elif args.command == "mentions":
|
||||
|
|
@ -184,12 +215,20 @@ def main() -> int:
|
|||
return completed.returncode
|
||||
else:
|
||||
last_heartbeat = 0.0
|
||||
seen_messages: set[str] = set()
|
||||
while True:
|
||||
if time.monotonic() - last_heartbeat >= max(15.0, args.heartbeat_seconds):
|
||||
emit(args.host, "HEARTBEAT", ["@ALL"], args.task_id, {"courier_online": True, "model_state": "UNKNOWN_UNLESS_CURRENT_HOST_AGENT_REFRESHES", "note": "courier heartbeat proves delivery-organ presence only"}, [])
|
||||
if time.monotonic() - last_heartbeat >= max(5.0, args.heartbeat_seconds):
|
||||
update_presence(args.host)
|
||||
last_heartbeat = time.monotonic()
|
||||
completed = subprocess.run(["python3", str(CONSOLE), "mentions", "--development-id", record["development_id"]], text=True, capture_output=True, timeout=30)
|
||||
print(completed.stdout, end="", flush=True)
|
||||
if completed.returncode == 0:
|
||||
inbox = json.loads(completed.stdout)
|
||||
fresh = [item for item in inbox.get("messages", []) if item.get("event_id") not in seen_messages]
|
||||
seen_messages.update(item.get("event_id") for item in inbox.get("messages", []) if item.get("event_id"))
|
||||
if fresh:
|
||||
print(json.dumps({"outcome":"PASS", "development_id":record["development_id"], "new_messages":fresh}, ensure_ascii=False, indent=2), flush=True)
|
||||
else:
|
||||
print(json.dumps({"outcome":"FAIL", "error":"MENTION_SCAN_FAILED", "detail":completed.stderr[-500:]}, ensure_ascii=False), flush=True)
|
||||
time.sleep(max(0.5, args.interval))
|
||||
if args.command != "watch":
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class MultipathConsoleTest(unittest.TestCase):
|
|||
self.assertFalse(value["panel_persisted"])
|
||||
self.assertTrue(value["same_persona_not_four_personas"])
|
||||
self.assertEqual(value["persona_id"], "ICE-P-ZY001")
|
||||
contract = MODULE.load(MODULE.CONTRACT)
|
||||
self.assertEqual(contract["presence_contract"]["semantics"], "EPHEMERAL_ATOMIC_OVERWRITE_COURIER_PRESENCE_NOT_HISTORY")
|
||||
|
||||
def test_time_moves_and_uses_world_time_channel(self):
|
||||
first = MODULE.live_time()
|
||||
|
|
|
|||
Loading…
Reference in a new issue