fix(kanban): wake the origin on review handoffs and triage escalations
`review_requested` and `block_loop_detected` are terminal event kinds that hand a decision back to the origin subscriber, but neither was listed in the gateway notifier's `_WAKE_KINDS`. A `notify+wake` subscription therefore got the passive ping only and the origin agent never took a turn — so an agent that delegated implementation work slept through the "ready for review" handoff and through a task being routed to triage, while the equivalent `blocked` event woke it. Add both kinds to the wake set, add their status strings to the synthetic wake message in every locale, and document which events wake.
This commit is contained in:
@@ -781,7 +781,16 @@ class GatewayKanbanWatchersMixin:
|
|||||||
# claim exactly like a failed send() above, so the
|
# claim exactly like a failed send() above, so the
|
||||||
# next tick retries.
|
# next tick retries.
|
||||||
task_terminal = task and task.status == "archived"
|
task_terminal = task and task.status == "archived"
|
||||||
_WAKE_KINDS = ("completed", "gave_up", "crashed", "timed_out", "blocked")
|
# Kinds that hand a decision back to the origin, so the
|
||||||
|
# origin has to take a turn. ``review_requested`` (the
|
||||||
|
# implementation is done and waits for a reviewer) and
|
||||||
|
# ``block_loop_detected`` (routed to triage) belong here
|
||||||
|
# for the same reason ``blocked`` does. ``status`` /
|
||||||
|
# ``archived`` / ``unblocked`` stay out: bookkeeping.
|
||||||
|
_WAKE_KINDS = (
|
||||||
|
"completed", "gave_up", "crashed", "timed_out",
|
||||||
|
"blocked", "review_requested", "block_loop_detected",
|
||||||
|
)
|
||||||
_wake_kinds = (
|
_wake_kinds = (
|
||||||
{ev.kind for ev in d["events"] if ev.kind in _WAKE_KINDS}
|
{ev.kind for ev in d["events"] if ev.kind in _WAKE_KINDS}
|
||||||
if wake_agent
|
if wake_agent
|
||||||
@@ -818,6 +827,8 @@ class GatewayKanbanWatchersMixin:
|
|||||||
if "crashed" in _wake_kinds: _parts.append(t("gateway.kanban.wake.crashed"))
|
if "crashed" in _wake_kinds: _parts.append(t("gateway.kanban.wake.crashed"))
|
||||||
if "timed_out" in _wake_kinds: _parts.append(t("gateway.kanban.wake.timed_out"))
|
if "timed_out" in _wake_kinds: _parts.append(t("gateway.kanban.wake.timed_out"))
|
||||||
if "blocked" in _wake_kinds: _parts.append(t("gateway.kanban.wake.blocked"))
|
if "blocked" in _wake_kinds: _parts.append(t("gateway.kanban.wake.blocked"))
|
||||||
|
if "review_requested" in _wake_kinds: _parts.append(t("gateway.kanban.wake.review_requested"))
|
||||||
|
if "block_loop_detected" in _wake_kinds: _parts.append(t("gateway.kanban.wake.block_loop_detected"))
|
||||||
_status = t("gateway.kanban.wake.status_joiner").join(_parts) or t("gateway.kanban.wake.status_default")
|
_status = t("gateway.kanban.wake.status_joiner").join(_parts) or t("gateway.kanban.wake.status_default")
|
||||||
_synth = t(
|
_synth = t(
|
||||||
"gateway.kanban.wake.message",
|
"gateway.kanban.wake.message",
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ gateway:
|
|||||||
crashed: "تعطّل (خرج العامل)؛ سيعيد المُوزّع المحاولة"
|
crashed: "تعطّل (خرج العامل)؛ سيعيد المُوزّع المحاولة"
|
||||||
timed_out: "انتهت المهلة؛ سيعيد المُوزّع المحاولة"
|
timed_out: "انتهت المهلة؛ سيعيد المُوزّع المحاولة"
|
||||||
blocked: "محجوبة؛ تحتاج إلى انتباه"
|
blocked: "محجوبة؛ تحتاج إلى انتباه"
|
||||||
|
review_requested: "أُحيلت للمراجعة؛ اكتمل التنفيذ"
|
||||||
|
block_loop_detected: "أُحيلت إلى الفرز بعد تكرار الحجب؛ تحتاج إلى قرار"
|
||||||
status_default: "تغيّرت الحالة"
|
status_default: "تغيّرت الحالة"
|
||||||
status_joiner: "، "
|
status_joiner: "، "
|
||||||
message: "[kanban] المهمة {task_id} {status}.\nالعنوان: {title}\nالمُكلَّف: @{assignee}\nاللوحة: {board}\n\nراجع النتيجة أو قرّر الخطوة التالية."
|
message: "[kanban] المهمة {task_id} {status}.\nالعنوان: {title}\nالمُكلَّف: @{assignee}\nاللوحة: {board}\n\nراجع النتيجة أو قرّر الخطوة التالية."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -181,6 +181,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "crashed (worker exited); dispatcher will retry"
|
crashed: "crashed (worker exited); dispatcher will retry"
|
||||||
timed_out: "timed out; dispatcher will retry"
|
timed_out: "timed out; dispatcher will retry"
|
||||||
blocked: "blocked; needs attention"
|
blocked: "blocked; needs attention"
|
||||||
|
review_requested: "handed off for review; the implementation is done"
|
||||||
|
block_loop_detected: "routed to triage after repeated blocks; needs a decision"
|
||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ gateway:
|
|||||||
crashed: "崩溃(worker 异常退出),dispatcher 将重试"
|
crashed: "崩溃(worker 异常退出),dispatcher 将重试"
|
||||||
timed_out: "超时,dispatcher 将重试"
|
timed_out: "超时,dispatcher 将重试"
|
||||||
blocked: "被阻塞,需要处理"
|
blocked: "被阻塞,需要处理"
|
||||||
|
review_requested: "已交付评审;实现已完成"
|
||||||
|
block_loop_detected: "反复阻塞后被转入待分诊,需要做出决定"
|
||||||
status_default: "状态变化"
|
status_default: "状态变化"
|
||||||
status_joiner: ","
|
status_joiner: ","
|
||||||
message: "[kanban] 任务 {task_id} {status}。\n标题: {title}\n执行者: @{assignee}\n看板: {board}\n\n请检查结果或决定下一步动作。"
|
message: "[kanban] 任务 {task_id} {status}。\n标题: {title}\n执行者: @{assignee}\n看板: {board}\n\n请检查结果或决定下一步动作。"
|
||||||
|
|||||||
@@ -622,3 +622,116 @@ def test_notifier_delivers_block_loop_detected_triage_ping(tmp_path, monkeypatch
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
assert remaining == []
|
assert remaining == []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Handoffs that hand a decision back to the origin must wake it, not only ping
|
||||||
|
# it: `review_requested` (implementation done, waiting for a reviewer) and
|
||||||
|
# `block_loop_detected` (routed to triage) are terminal kinds just like
|
||||||
|
# `blocked`.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _wake_text(adapter):
|
||||||
|
"""Text of the single synthetic wake turn injected into the adapter."""
|
||||||
|
assert len(adapter.handled) == 1, (
|
||||||
|
f"expected exactly one wake turn, got {len(adapter.handled)}"
|
||||||
|
)
|
||||||
|
return getattr(adapter.handled[0], "text", "") or ""
|
||||||
|
|
||||||
|
|
||||||
|
def _review_handoff_task(*, delivery_mode="notify+wake"):
|
||||||
|
conn = kb.connect()
|
||||||
|
try:
|
||||||
|
tid = kb.create_task(
|
||||||
|
conn,
|
||||||
|
title="implement the thing",
|
||||||
|
assignee="worker",
|
||||||
|
session_id="agent:main:telegram:dm:chat-1",
|
||||||
|
)
|
||||||
|
kb.add_notify_sub(
|
||||||
|
conn,
|
||||||
|
task_id=tid,
|
||||||
|
platform="telegram",
|
||||||
|
chat_id="chat-1",
|
||||||
|
chat_type="dm",
|
||||||
|
delivery_mode=delivery_mode,
|
||||||
|
)
|
||||||
|
kb.claim_task(conn, tid)
|
||||||
|
run_id = kb.get_task(conn, tid).current_run_id
|
||||||
|
assert kb.request_review(
|
||||||
|
conn, tid, summary="implementation done", expected_run_id=run_id,
|
||||||
|
) is True
|
||||||
|
return tid
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_requested_wakes_the_origin_session(tmp_path, monkeypatch):
|
||||||
|
"""A review handoff wakes the origin on top of the passive ping."""
|
||||||
|
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "review-wake.db"))
|
||||||
|
kb.init_db()
|
||||||
|
tid = _review_handoff_task()
|
||||||
|
|
||||||
|
adapter = RecordingAdapter()
|
||||||
|
runner = _make_runner(adapter)
|
||||||
|
asyncio.run(_run_one_notifier_tick(monkeypatch, runner))
|
||||||
|
|
||||||
|
assert len(adapter.sent) == 1, "the passive review ping is unchanged"
|
||||||
|
assert "ready for review" in adapter.sent[0]["text"]
|
||||||
|
assert tid in _wake_text(adapter)
|
||||||
|
|
||||||
|
|
||||||
|
def test_block_loop_detected_wakes_the_origin_session(tmp_path, monkeypatch):
|
||||||
|
"""A triage escalation wakes the origin so a decision gets made."""
|
||||||
|
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "triage-wake.db"))
|
||||||
|
kb.init_db()
|
||||||
|
|
||||||
|
conn = kb.connect()
|
||||||
|
try:
|
||||||
|
tid = kb.create_task(
|
||||||
|
conn,
|
||||||
|
title="loops forever",
|
||||||
|
assignee="worker",
|
||||||
|
session_id="agent:main:telegram:dm:chat-1",
|
||||||
|
)
|
||||||
|
kb.add_notify_sub(
|
||||||
|
conn,
|
||||||
|
task_id=tid,
|
||||||
|
platform="telegram",
|
||||||
|
chat_id="chat-1",
|
||||||
|
chat_type="dm",
|
||||||
|
delivery_mode="notify+wake",
|
||||||
|
)
|
||||||
|
kb._append_event(
|
||||||
|
conn, tid, "block_loop_detected",
|
||||||
|
{"reason": "needs credentials", "kind": "needs_input",
|
||||||
|
"recurrences": 2, "limit": kb.BLOCK_RECURRENCE_LIMIT},
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
adapter = RecordingAdapter()
|
||||||
|
runner = _make_runner(adapter)
|
||||||
|
asyncio.run(_run_one_notifier_tick(monkeypatch, runner))
|
||||||
|
|
||||||
|
assert len(adapter.sent) == 1
|
||||||
|
assert tid in _wake_text(adapter)
|
||||||
|
|
||||||
|
|
||||||
|
def test_review_requested_does_not_wake_a_notify_only_subscription(
|
||||||
|
tmp_path, monkeypatch,
|
||||||
|
):
|
||||||
|
"""delivery_mode still decides whether a wake-worthy kind wakes at all."""
|
||||||
|
monkeypatch.setenv("HERMES_KANBAN_DB", str(tmp_path / "review-notify.db"))
|
||||||
|
kb.init_db()
|
||||||
|
_review_handoff_task(delivery_mode="notify")
|
||||||
|
|
||||||
|
adapter = RecordingAdapter()
|
||||||
|
runner = _make_runner(adapter)
|
||||||
|
asyncio.run(_run_one_notifier_tick(monkeypatch, runner))
|
||||||
|
|
||||||
|
assert len(adapter.sent) == 1
|
||||||
|
assert adapter.handled == [], (
|
||||||
|
"notify-only subscriptions must not be woken by a review handoff"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1035,6 +1035,8 @@ A subscription removes itself automatically once the task reaches `done` or `arc
|
|||||||
|
|
||||||
A "wake" forges a synthetic inbound message to the destination gateway agent so it takes a normal turn (reads the comment + result, reasons, replies) instead of getting a one-line passive notification. It only fires when the notifier runs inside a live gateway process; otherwise a `notify+wake` subscription still delivers its passive message, while a `wake`-only subscription does nothing in that process.
|
A "wake" forges a synthetic inbound message to the destination gateway agent so it takes a normal turn (reads the comment + result, reasons, replies) instead of getting a one-line passive notification. It only fires when the notifier runs inside a live gateway process; otherwise a `notify+wake` subscription still delivers its passive message, while a `wake`-only subscription does nothing in that process.
|
||||||
|
|
||||||
|
**Which events wake.** The ones that hand a decision back to the origin: `completed`, `blocked`, `gave_up`, `crashed`, `timed_out`, `review_requested` (a worker finished the implementation and handed off via `kanban_request_review`) and `block_loop_detected` (the task was routed to `triage` after repeated blocks). `status`, `archived` and `unblocked` are delivered but never wake — they are bookkeeping transitions, not decisions.
|
||||||
|
|
||||||
`--chat-type` (`dm` | `group` | `channel` | `thread`) records the originating chat's type so a woken turn resolves the operator's **real** session: `build_session_key` keys groups, channels, and threads differently from DMs, so an inaccurate `chat_type` would route the wake into a separate, context-less session. The `/kanban` auto-subscribe and slash-command paths capture this automatically — you only set it by hand when subscribing a chat from a script or cron. Omit it to leave an existing subscription unchanged (new subscriptions default to `dm`).
|
`--chat-type` (`dm` | `group` | `channel` | `thread`) records the originating chat's type so a woken turn resolves the operator's **real** session: `build_session_key` keys groups, channels, and threads differently from DMs, so an inaccurate `chat_type` would route the wake into a separate, context-less session. The `/kanban` auto-subscribe and slash-command paths capture this automatically — you only set it by hand when subscribing a chat from a script or cron. Omit it to leave an existing subscription unchanged (new subscriptions default to `dm`).
|
||||||
|
|
||||||
### Multi-profile setups: delivery is profile-owned
|
### Multi-profile setups: delivery is profile-owned
|
||||||
|
|||||||
Reference in New Issue
Block a user