diff --git a/SKILL.md b/SKILL.md index 3ba6660..b331a9b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -132,6 +132,10 @@ description: 组织项目的多个 AI 会话并行工作,将进度、证据和 ## 可靠完成上报与通知背压 +咨询桥接按真实待发或在途消息分配,不因绑定存在就长期占位。空闲释放前核对worker、待交付回执和未知执行;只停止自有桥接,不终止模型writer。保留并发上限,按持久等待时间公平轮转;人类咨询及决策优先,普通事项达到老化窗口后按先到顺序提升,不能永久饥饿。断网时保留原消息、幂等键和回执,恢复后续传;可能已执行的输入先查原会话和关联回复,禁止换ID盲重放。具体调度与恢复见运行指南。 + +页面按证据区分已保存、排队、宿主送达、真实处理、关联回复和受阻。保存成功不等于送达,模型回复不等于业务验收;未知结果明确待核对。普通成功阶段收起保留可查,等待和受阻短提示,不堆叠技术回执。 + 执行负责人完成工作后,先把带稳定报告ID、任务版本、证据和检查结果的完成报告保存到私有持久 outbox,再按有界退避重试交付;服务落库回执确认后才标已上报。重试复用同一幂等ID,不重新执行业务操作;结果未知先对账,达到上限保留待核查状态,交副主控处理。对话中的“完成”不能替代可靠落库,执行完成也不等于集成验收完成;集成负责人另核对源产物、接口和验收证据。 通知按目标背压:同一目标最多一条未处理通知,后续事件保留在持久队列并按关联ID合并。排队accepted不等于处理完成,需核对真实目标turn/处理回执才能释放名额;未知结果不得通过换ID或清本地记录绕过。应用outbox、已知inflight与宿主原生队列分别记账;无法读取宿主总数或确认取消时,必须写未知/不支持,不能宣称原生队列已清空。现有机制与限制见运行指南。 diff --git a/agent_control/conversation.py b/agent_control/conversation.py index 535ae10..b92e160 100644 --- a/agent_control/conversation.py +++ b/agent_control/conversation.py @@ -21,6 +21,27 @@ def browser_body(text): return text[match.end():] if match else text +def delivery_fields(row): + """Expose evidence-based transport stages without private queue receipts.""" + if row.get('source') != 'human' or row.get('origin') != 'coordination': + return {} + status = row.get('status') + submission = row.get('submission') or {} + if isinstance(submission, str): + submission = json.loads(submission) + if status in ('uncertain', 'failed'): + return dict(delivery_stage='blocked', delivery_reason=( + '执行结果待核对,请勿重复发送' if status == 'uncertain' else '处理失败,请查看记录')) + if status == 'completed' and row.get('response_id'): + return dict(delivery_stage='replied') + if submission.get('state') == 'observed': + return dict(delivery_stage='processing') + if submission.get('state') == 'accepted': + return dict(delivery_stage='delivered') + # A local start or submitting record is not proof of host acceptance. + return dict(delivery_stage='queued') + + def project(rows, links): by_id = {row['id']: row for row in rows} metadata = {link['message_id']: link for link in links} @@ -101,5 +122,6 @@ def project(rows, links): if key in seen: continue seen.add(key) - result.append({**{k: row.get(k) for k in ('id', 'source', 'status', 'created')}, 'text': text, 'log_id':incident_links.get(group)}) + result.append({**{k: row.get(k) for k in ('id', 'source', 'status', 'created', 'reply_to')}, + **delivery_fields(row), 'text': text, 'log_id':incident_links.get(group)}) return result diff --git a/agent_control/store.py b/agent_control/store.py index 16bc5b4..f32b9b7 100644 --- a/agent_control/store.py +++ b/agent_control/store.py @@ -888,11 +888,13 @@ class Store: @staticmethod def public_message(row): + from .conversation import delivery_fields result = {k: row[k] for k in ('id','session_id','text','source','reply_to','status','created','response_id')} result['outcome'] = row['status'] if row['status'] in ('completed','failed','uncertain') else None result['origin'] = row['origin'] result['execution'] = 'started' if row['started'] else 'not_started' result['submission'] = json.loads(row['submission']) if row['submission'] else None + result.update(delivery_fields(dict(row))) return result def send(self, data): @@ -1905,9 +1907,16 @@ class Store: value=self._decision(row) if value['session_id']: session=self._get(db,'sessions',value['session_id']) + rows=db.execute("SELECT * FROM messages WHERE session_id=? AND source='human' AND origin='coordination' AND status IN ('queued','claimed','uncertain')", (value['session_id'],)).fetchall() + now=time.time() + queued=[m for m in rows if m['status']=='queued' or (m['status']=='claimed' and not m['started'] and (m['lease_until'] or 0)<=now)] + uncertain=[m for m in rows if m['status']=='uncertain' or (m['status']=='claimed' and m['started'] and (m['lease_until'] or 0)<=now)] + inflight=[m for m in rows if m not in queued and m not in uncertain] + demand=dict(queued=len(queued),inflight=len(inflight),uncertain=len(uncertain), + human_queued=len(queued),oldest_queued_at=min((m['created'] for m in queued),default=None)) bindings.append(dict(decision_id=value['id'],session_id=value['session_id'],thread_id=value['session_thread_id'], worktree=session['worktree'],status=value['status'],archive_status=value['archive_status'], - pending_messages=self._decision_pending(db,value))) + pending_messages=self._decision_pending(db,value),demand=demand)) return {'bindings':bindings} def decision_consultation(self, did, data=None): diff --git a/assets/dashboard/src/components/decision-chat.tsx b/assets/dashboard/src/components/decision-chat.tsx index 8b8a3ea..e68453d 100644 --- a/assets/dashboard/src/components/decision-chat.tsx +++ b/assets/dashboard/src/components/decision-chat.tsx @@ -1,3 +1,4 @@ +import { MessageDelivery } from '@/components/message-delivery' import { useEffect, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' @@ -44,7 +45,7 @@ export default function DecisionChat({ decision, token }: { decision: Decision; } return

本决策咨询

{(!bound || readOnly) &&

{!bound ? '咨询暂不可用。' : '已完成,只读。'}

} -
{messages.map(message =>

{message.source === 'human' ? '你' : '咨询助手'}

{message.text}

{message.source === 'human' && ['queued','claimed'].includes(message.status) &&

等待回复…

}{['failed','uncertain'].includes(message.status) &&

回复尚未确认,请勿重复发送。

}
)}{!messages.length &&

{!bound ? '暂无已接通的咨询会话。' : loaded ? '暂无咨询消息。' : '正在读取咨询历史…'}

}
+
{messages.map(message =>

{message.source === 'human' ? '你' : '咨询助手'}

{message.text}

)}{!messages.length &&

{!bound ? '暂无已接通的咨询会话。' : loaded ? '暂无咨询消息。' : '正在读取咨询历史…'}

}
{(error || draft.error) &&

{draft.error || error}

}
{local.error &&

{local.error}

}{local.error && }{draft.revision !== revision &&

这是版本 {draft.revision} 的咨询草稿,请重新确认当前问题。

{!readOnly && }
}{readOnly && draft.text &&

保留的未发送草稿(只读):{draft.text}

}
{!readOnly &&
{ event.preventDefault(); void send() }}>