Schedule consultation bridges on demand and preserve delivery evidence
This commit is contained in:
+35
-3
@@ -59,8 +59,26 @@ def notification_text(n):
|
||||
+json.dumps(data,ensure_ascii=False)+'\n请核实事件与影响,按既定权限处理。面向用户仅汇报持续重大影响、需行动或待决策;短暂自动恢复只进日志,不向人类入口转发。若有log_id,给一句摘要并链接日志页#/logs?incident=<log_id>,不复制原载荷;重复状态不打扰。消息已投递不代表故障已修复。')
|
||||
|
||||
|
||||
def target_ready(cfg,records,thread,cache):
|
||||
if not cfg.get('enforce_backpressure'):
|
||||
def schedule_notifications(notifications,state,now):
|
||||
"""Prefer decisions while aging waiting work into FIFO; persist across restarts.
|
||||
|
||||
This changes dispatch order only, never authorization, receipt or event identity.
|
||||
A busy/uncertain target remains blocked regardless of waiting time.
|
||||
"""
|
||||
waiting=state.setdefault('notification_waiting',{})
|
||||
def identity(n):
|
||||
return ('decision:' if 'decision' in n else 'eta:' if 'checkpoint' in n else 'hook:')+n['notification_id']
|
||||
for n in notifications:
|
||||
waiting.setdefault(identity(n),now)
|
||||
def rank(n):
|
||||
first=waiting[identity(n)]
|
||||
priority=0 if 'decision' in n else 1 if n.get('hook',{}).get('severity')=='critical' else 2 if 'hook' in n else 3
|
||||
return (0 if now-first>=120 else priority+1,first,identity(n))
|
||||
return sorted(notifications,key=rank)
|
||||
|
||||
|
||||
def target_ready(cfg,records,thread,cache,force_reconcile=False):
|
||||
if not cfg.get('enforce_backpressure') and not force_reconcile:
|
||||
return True
|
||||
limit=cfg.get('max_inflight_per_target',1)
|
||||
if not isinstance(limit,int) or isinstance(limit,bool) or not 1<=limit<=3:
|
||||
@@ -96,6 +114,15 @@ def deliver(client,cfg,state,path):
|
||||
records=state.setdefault('deliveries',{})
|
||||
errors=[]
|
||||
cache={}
|
||||
# Fact answers enter the native consultation thread directly. Reconcile their
|
||||
# completion even with an empty outbox, so a demand-driven mirror can retire.
|
||||
for thread in {r['thread_id'] for r in records.values()
|
||||
if r.get('event_kind')=='fact_answered' and not r.get('processing_done')}:
|
||||
try:
|
||||
target_ready(cfg,records,thread,cache,force_reconcile=True)
|
||||
except Exception:
|
||||
errors.append('fact_answer_reconciliation_unavailable')
|
||||
atomic_json(path,state)
|
||||
if cfg.get('coalesce_stale_hooks'):
|
||||
# Upgrade old accepted-but-unacked receipts before pruning pending data.
|
||||
# Unknown submissions are protected, never silently cancelled or replayed.
|
||||
@@ -128,7 +155,9 @@ def deliver(client,cfg,state,path):
|
||||
notifications += batch['notifications']
|
||||
queue_counts[endpoint]=batch.get('pending_count',len(batch['notifications']))
|
||||
except Exception:errors.append('outbox_unavailable')
|
||||
notifications.sort(key=lambda n:0 if 'decision' in n else 1 if n.get('hook',{}).get('severity')=='critical' else 2 if 'hook' in n else 3)
|
||||
notifications=schedule_notifications(notifications,state,time.time())
|
||||
# Waiting age must survive offline delivery/ACK failures, not just successful sends.
|
||||
atomic_json(path,state)
|
||||
task_owners={}
|
||||
if cfg.get('execution_notification_targets'):
|
||||
snapshot=client.call('/api/state')
|
||||
@@ -161,6 +190,8 @@ def deliver(client,cfg,state,path):
|
||||
if 'decision' in n:
|
||||
client.call('/api/decision-notifications/submitting',{'notification_id':n['notification_id']})
|
||||
old={'kind':'hook' if 'hook' in n else 'decision' if 'decision' in n else 'eta','notification_ids':([item['notification_id'] for item in notifications if item.get('hook')==n.get('hook')] if internal_event else [n['notification_id']]),'created_at':time.time(),'thread_id':thread,'input_sha256':digest,'state':'submitting'}
|
||||
if 'decision' in n:
|
||||
old.update(decision_id=n['decision']['id'],event_kind=n.get('event',{}).get('kind'))
|
||||
records[key]=old
|
||||
atomic_json(path,state)
|
||||
if 'hook' in n and cfg.get('coalesce_stale_hooks'):
|
||||
@@ -177,6 +208,7 @@ def deliver(client,cfg,state,path):
|
||||
# Crash after queue acceptance: reconcile exact actual input, never requeue.
|
||||
with AppServer(cfg['codex']) as reader:
|
||||
meta,turns=reader.history(thread)
|
||||
if meta.get('id')!=thread:raise ValueError('Unexpected reconciliation thread identity')
|
||||
observed=next((t['id'] for t in turns if any(
|
||||
i.get('type')=='userMessage' and '\n'.join(p.get('text','') for p in i.get('content',[]) if p.get('type')=='text')==prompt
|
||||
for i in t.get('items',[]))),None)
|
||||
|
||||
Reference in New Issue
Block a user