#!/usr/bin/env python3 """Bridge already bound decision threads; host creation and archive stay explicit.""" import argparse import hashlib import json import os from pathlib import Path import re import subprocess import sys import time from control import Client from worker import atomic_json,session_lock def busy(path): if not path.exists():return False state=json.loads(path.read_text(encoding='utf-8')) return bool(state.get('inflight') or state.get('pending_completion') or state.get('needs_reconciliation')) def reconcile_completed(path,client,session_id,thread_id): """Clear only a proven terminal original execution, never submit another input.""" state=json.loads(path.read_text(encoding='utf-8')) record=state.get('inflight') or {} proof=record.get('submission') or {} if not record.get('message_id') or not proof.get('input_sha256'):return False from urllib.parse import quote rows=client.call('/api/messages?session_id='+quote(session_id,safe=''))['messages'] original=next((m for m in rows if m['id']==record['message_id']),None) if not original:return False reply=next((m for m in rows if m['id']==original.get('response_id')),None) remote=original.get('submission') or {} if (not reply or original.get('status') not in ('completed','failed') or remote.get('thread_id')!=thread_id or remote.get('input_sha256')!=proof['input_sha256'] or (proof.get('turn_id') and remote.get('turn_id')!=proof['turn_id'])):return False state.setdefault('delivery_receipts',{})[record['message_id']]=remote for key in ('inflight','pending_completion','needs_reconciliation'):state.pop(key,None) atomic_json(path,state) return True def child_config(base,binding,root): did=binding['decision_id'];sid=binding['session_id'];thread=binding['thread_id'] if any(not isinstance(v,str) or not re.fullmatch(r'[A-Za-z0-9_.:-]{1,128}',v) for v in (did,sid)): raise ValueError('Invalid decision binding') if not re.fullmatch(r'[0-9a-fA-F-]{36}',thread):raise ValueError('Invalid real thread identity') cwd=Path(binding['worktree']).expanduser().resolve() if not cwd.is_dir():raise ValueError('Bound thread directory unavailable') runtime=root/hashlib.sha256(did.encode()).hexdigest()[:24] runtime.mkdir(parents=True,exist_ok=True,mode=0o700) cfg={key:base[key] for key in ('control','codex') if key in base} cfg.update(mode='desktop_queue',session_id=sid,thread_id=thread,cwd=str(cwd), worker_id='decision-'+hashlib.sha256(sid.encode()).hexdigest()[:20], state_file=str(runtime/'worker.json'),mirror_state_file=str(runtime/'mirror.json'), include_progress_snapshot=False,poll_seconds=5,mirror_seconds=5, max_turn_seconds=base.get('max_turn_seconds',600)) cfg['instructions']=('你是独立决策咨询会话,仅维护决策 '+did+'。只可通过初始化时提供的本决策有限凭据读取历史、解释或修订该决策、向主控提出事实核实请求。' '不得修改项目代码、系统、其他任务或决策,不执行方案、接受用户提交、伪造检查或归档。需要主控事实时使用本决策facts接口并保留关联ID,不重复催问。' '用户提交或主控接受不表示新增授权。一般项目问题与待授权事项定向交项目agent对话入口,不另起一般人类沟通。当前网页消息只用于本决策咨询。') return cfg,runtime class Fleet: def __init__(self,cfg): self.cfg=cfg settings=cfg['decision_fleet'] self.root=Path(settings['runtime_root']).expanduser().resolve() self.root.mkdir(parents=True,exist_ok=True,mode=0o700) self.limit=settings.get('max_active',3) if isinstance(self.limit,bool) or not isinstance(self.limit,int) or not 1<=self.limit<=3: raise ValueError('Decision bridge bound must be between one and three') self.children={} self.health=Path(cfg['state_file']).parent/'decision_fleet-health.json' def stop(self,did,force=False): child=self.children[did] # Single-cycle children exit naturally; never interrupt a native writer. if any(p.poll() is None for p in child['processes'].values()):return False for stream in child['logs'].values():stream.close() del self.children[did] return True def tick(self,bindings): now=time.time();problems=[];live={};threads=set();sessions=set() journal_path=self.root/'schedule.json' journal=json.loads(journal_path.read_text(encoding='utf-8')) if journal_path.exists() else {} notifications={} if self.cfg.get('notification_state_file'): notification_path=Path(self.cfg['notification_state_file']) if notification_path.exists():notifications=json.loads(notification_path.read_text(encoding='utf-8')).get('deliveries',{}) for binding in bindings: did=binding['decision_id'] if did in live or binding['thread_id'] in threads or binding['session_id'] in sessions: raise ValueError('Duplicate decision, session or thread binding') live[did]=binding;threads.add(binding['thread_id']);sessions.add(binding['session_id']) for did in list(self.children): binding=live.get(did) child=self.children[did] mirror=child['processes'].get('mirror') if mirror and mirror.poll()==0: journal.setdefault(did,{}).setdefault('fact_synced',{}).update(child.get('terminal_facts',{})) if binding and child_config(self.cfg,binding,self.root)[0]!=self.children[did]['cfg']: problems.append('binding_changed:'+did);continue if not self.stop(did) and (not binding or binding.get('archive_status')=='archived'): problems.append('archive_requires_reconciliation:'+did) candidates=[] for did,binding in live.items(): if binding.get('archive_status')=='archived':continue candidate,runtime=child_config(self.cfg,binding,self.root) demand=binding.get('demand') state_path=Path(candidate['state_file']) local_busy=busy(state_path) worker=self.children.get(did,{}).get('processes',{}).get('worker') worker_alive=worker is not None and worker.poll() is None if local_busy and not worker_alive and demand is not None and not demand.get('uncertain',0) and not demand.get('inflight',0): try: if reconcile_completed(state_path,Client(self.cfg['control']),binding['session_id'],binding['thread_id']):local_busy=False except Exception:pass if demand is None: problems.append('queue_summary_unavailable:'+did);demand={} uncertain=bool(demand.get('uncertain',0) or local_busy) facts={key:r for key,r in notifications.items() if r.get('decision_id')==did and r.get('event_kind')=='fact_answered'} terminal_facts={key:str(r.get('completed_at') or r.get('turn_id') or 'done') for key,r in facts.items() if r.get('processing_done')} synced=journal.get(did,{}).get('fact_synced',{}) fact_demand=any(not r.get('processing_done') for r in facts.values()) or any(synced.get(key)!=value for key,value in terminal_facts.items()) needed=bool(demand.get('queued',0) or demand.get('inflight',0) or uncertain or fact_demand) if not needed: if did in journal:journal[did].pop('waiting_since',None) continue record=journal.setdefault(did,{}) record.setdefault('waiting_since',min(now,demand.get('oldest_queued_at') or now)) if did in self.children: if uncertain and not worker_alive:problems.append('execution_requires_reconciliation:'+did) continue # Finite human boost: old ordinary work eventually precedes new human work. score=now-record['waiting_since']+(60 if demand.get('human_queued',0) else 0) candidates.append((-score,record['waiting_since'],record.get('last_served',0),did,binding,candidate,runtime,uncertain,terminal_facts)) candidates.sort(key=lambda row:row[:4]);waiting=0 for _,_,_,did,binding,candidate,runtime,uncertain,terminal_facts in candidates: if len(self.children)>=self.limit:waiting+=1;continue config_path=runtime/'bridge.json' if config_path.exists() and json.loads(config_path.read_text(encoding='utf-8'))!=candidate: problems.append('saved_binding_changed:'+did);continue atomic_json(config_path,candidate) child={'cfg':candidate,'runtime':runtime,'path':config_path,'processes':{},'logs':{},'terminal_facts':terminal_facts} self.children[did]=child journal[did].update(waiting_since=now,last_served=now) atomic_json(journal_path,journal) roles=('mirror',) if uncertain or binding.get('demand',{}).get('inflight',0) or not binding.get('demand',{}).get('queued',0) else ('worker','mirror') if uncertain:problems.append('execution_requires_reconciliation:'+did) for role in roles: stream=open(runtime/(role+'.log'),'a',encoding='utf-8');child['logs'][role]=stream command=[sys.executable,'-X','utf8',str(Path(__file__).with_name(role+'.py')),'--config',str(config_path),'--once'] child['processes'][role]=subprocess.Popen(command,stdout=stream,stderr=stream, **({'creationflags':subprocess.CREATE_NO_WINDOW} if os.name=='nt' else {})) atomic_json(journal_path,journal) snapshot={'status':'failed' if problems else 'ok','updated':now,'active':len(self.children), 'waiting':waiting,'problems':problems,'bindings':{did:{'session_id':c['cfg']['session_id'], 'thread_id':c['cfg']['thread_id'],'processes':{r:{'pid':p.pid,'running':p.poll() is None} for r,p in c['processes'].items()}} for did,c in self.children.items()}} atomic_json(self.health,snapshot) return snapshot def close(self): # Active cycles retain their durable state and host writer locks. for did in list(self.children): for stream in self.children[did]['logs'].values():stream.close() self.stop(did) def main(): parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--config',required=True) parser.add_argument('--once',action='store_true');args=parser.parse_args() cfg=json.loads(Path(args.config).read_text(encoding='utf-8-sig'));fleet=Fleet(cfg) if os.name=='posix':os.umask(0o077) client=Client(cfg['control']) with session_lock('decision-fleet:'+str(fleet.root)): try: while True: try:fleet.tick(client.call('/api/decision-bridge-bindings')['bindings']) except Exception: atomic_json(fleet.health,{'status':'failed','updated':time.time(),'reason':'Inspect decision bindings and private bridge state'}) if args.once:raise if args.once:return time.sleep(5) finally:fleet.close() if __name__=='__main__':main()