mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(orchestrator): let a failed Board Program exploration retry (#706)
`_board_dispatched` is an in-memory, never-expiring set of (agent_slug, task_id). The solo exploration dispatchers consulted it, so an exploration whose propose verb rejected was never retried for the life of the process — Periscope, Sentinel, Scales and Barfly each spawned once on 2026-07-25, failed, and sat PENDING until the stack restarted. The guard was written for the two-reviewer board REVIEW pass, where it is correct: a reviewer has no verb to advance the task, so a respawn can only loop. An explorer is the opposite — `propose_*` is exactly such a verb, so a respawn can and should advance it. Drop it from the 15 `_dispatch_*_exploration` functions. Bounding falls to `_pm_respawn_should_gate`, which is what actually bounds a loop: DB-persisted, reset by a status change, and cooled down so a deploy that fixes the cause lets the work resume. `_dispatch_board_reviewer` keeps the set (its `_board_review_complete` reads it as a has-run signal) as does vault curation (same-process race guard behind its own durable marker). The 14 `*_dispatch_is_one_shot` tests asserted the old contract on the false rationale that board roles have no progression verb; they now assert a second tick re-attempts. Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -1230,6 +1230,17 @@ class AgentOrchestrator:
|
|||||||
# review pass per assigned task: they have no verb to claim, plan,
|
# review pass per assigned task: they have no verb to claim, plan,
|
||||||
# delegate, or complete, so a respawn cannot advance the task and would
|
# delegate, or complete, so a respawn cannot advance the task and would
|
||||||
# just loop. Tracks (agent_slug, task_id) already dispatched.
|
# just loop. Tracks (agent_slug, task_id) already dispatched.
|
||||||
|
#
|
||||||
|
# Scope is the two-reviewer board REVIEW pass (plus vault curation's
|
||||||
|
# same-process race guard, which has its own durable marker behind it)
|
||||||
|
# — NOT the solo Board Program exploration cycles. Those used it too
|
||||||
|
# until 2026-07-26, and because the set never expires and lives only in
|
||||||
|
# memory, an exploration whose propose verb rejected was never retried
|
||||||
|
# for the life of the process: Periscope, Sentinel, Scales and Barfly
|
||||||
|
# each spawned once, failed, and sat PENDING until a restart. They now
|
||||||
|
# rely on ``_pm_respawn_should_gate`` alone, which is the guard that
|
||||||
|
# actually bounds a loop — DB-persisted, reset by a status change, and
|
||||||
|
# cooled down so a deploy that fixes the cause lets the work resume.
|
||||||
self._board_dispatched: set[tuple[str, str]] = set()
|
self._board_dispatched: set[tuple[str, str]] = set()
|
||||||
# Cross-tick damper for notification-triggered spawns (escalation /
|
# Cross-tick damper for notification-triggered spawns (escalation /
|
||||||
# approval / audit / a2a). Those dispatchers carry no task_id, so the
|
# approval / audit / a2a). Those dispatchers carry no task_id, so the
|
||||||
@@ -12878,7 +12889,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
"Approve & Start" notification fires. ``propose_roadmap`` (not this
|
"Approve & Start" notification fires. ``propose_roadmap`` (not this
|
||||||
dispatcher) marks the cycle authored (a ``roadmap_cycle`` marker); this
|
dispatcher) marks the cycle authored (a ``roadmap_cycle`` marker); this
|
||||||
only ever spawns once per task while that marker is absent, reusing the
|
only ever spawns once per task while that marker is absent, reusing the
|
||||||
same one-shot ``_board_dispatched`` tracker + respawn breaker every
|
respawn breaker every
|
||||||
other board dispatch uses.
|
other board dispatch uses.
|
||||||
"""
|
"""
|
||||||
task_id = str(task.get("id"))
|
task_id = str(task.get("id"))
|
||||||
@@ -12888,12 +12899,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
po_slug = "product-owner"
|
po_slug = "product-owner"
|
||||||
if self._is_agent_active(po_slug):
|
if self._is_agent_active(po_slug):
|
||||||
return
|
return
|
||||||
key = (po_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(po_slug, task):
|
if await self._pm_respawn_should_gate(po_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info("Spawning Product Owner for roadmap exploration", task_id=task_id)
|
logger.info("Spawning Product Owner for roadmap exploration", task_id=task_id)
|
||||||
prior_context = await self._board_program_prior_context("roadmap")
|
prior_context = await self._board_program_prior_context("roadmap")
|
||||||
market_brief_context = await self._periscope_brief_context()
|
market_brief_context = await self._periscope_brief_context()
|
||||||
@@ -12931,7 +12938,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
"""One-shot Product-Owner spawn to author a Pest Control bug hunt.
|
"""One-shot Product-Owner spawn to author a Pest Control bug hunt.
|
||||||
|
|
||||||
Mirrors ``_dispatch_roadmap_exploration`` exactly (PO-solo, the same
|
Mirrors ``_dispatch_roadmap_exploration`` exactly (PO-solo, the same
|
||||||
one-shot ``_board_dispatched`` tracker + respawn breaker, the same
|
respawn breaker
|
||||||
"already authored" marker pre-check — ``propose_bug_hunt`` marks it
|
"already authored" marker pre-check — ``propose_bug_hunt`` marks it
|
||||||
via the ``pest_hunt`` marker, not this dispatcher). The extra step is
|
via the ``pest_hunt`` marker, not this dispatcher). The extra step is
|
||||||
the server-assembled evidence context (rework hotspots + findings-
|
the server-assembled evidence context (rework hotspots + findings-
|
||||||
@@ -12944,12 +12951,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
po_slug = "product-owner"
|
po_slug = "product-owner"
|
||||||
if self._is_agent_active(po_slug):
|
if self._is_agent_active(po_slug):
|
||||||
return
|
return
|
||||||
key = (po_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(po_slug, task):
|
if await self._pm_respawn_should_gate(po_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Spawning Product Owner for pest-control exploration", task_id=task_id
|
"Spawning Product Owner for pest-control exploration", task_id=task_id
|
||||||
)
|
)
|
||||||
@@ -12984,7 +12987,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
"""One-shot Product-Owner spawn to author a Scales rebalance plan.
|
"""One-shot Product-Owner spawn to author a Scales rebalance plan.
|
||||||
|
|
||||||
Mirrors ``_dispatch_pest_control_exploration`` exactly (PO-solo, the
|
Mirrors ``_dispatch_pest_control_exploration`` exactly (PO-solo, the
|
||||||
same one-shot ``_board_dispatched`` tracker + respawn breaker, the
|
respawn breaker the
|
||||||
same "already authored" marker pre-check — ``propose_rebalance``
|
same "already authored" marker pre-check — ``propose_rebalance``
|
||||||
marks it via the ``rebalance_plan`` marker, not this dispatcher). The
|
marks it via the ``rebalance_plan`` marker, not this dispatcher). The
|
||||||
extra step is the server-assembled stale-backlog snapshot the PO
|
extra step is the server-assembled stale-backlog snapshot the PO
|
||||||
@@ -12997,12 +13000,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
po_slug = "product-owner"
|
po_slug = "product-owner"
|
||||||
if self._is_agent_active(po_slug):
|
if self._is_agent_active(po_slug):
|
||||||
return
|
return
|
||||||
key = (po_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(po_slug, task):
|
if await self._pm_respawn_should_gate(po_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info("Spawning Product Owner for scales exploration", task_id=task_id)
|
logger.info("Spawning Product Owner for scales exploration", task_id=task_id)
|
||||||
prior_context = await self._board_program_prior_context("scales")
|
prior_context = await self._board_program_prior_context("scales")
|
||||||
evidence_context = await self._scales_evidence_context()
|
evidence_context = await self._scales_evidence_context()
|
||||||
@@ -13039,8 +13038,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
pest-control "already authored" marker shape): no pre-check is
|
pest-control "already authored" marker shape): no pre-check is
|
||||||
needed — ``propose_postmortem`` completes this task atomically, so a
|
needed — ``propose_postmortem`` completes this task atomically, so a
|
||||||
successful call stops it matching the PENDING fetch on the next
|
successful call stops it matching the PENDING fetch on the next
|
||||||
tick. Reuses the same one-shot ``_board_dispatched`` tracker +
|
tick. Reuses respawn breaker every other board dispatch uses. EVENT-triggered
|
||||||
respawn breaker every other board dispatch uses. EVENT-triggered
|
|
||||||
(spec §4): this task only ever exists because
|
(spec §4): this task only ever exists because
|
||||||
``CoronerEngine.open_for_incident`` opened it — there is no LEARN
|
``CoronerEngine.open_for_incident`` opened it — there is no LEARN
|
||||||
"prior cycles" injection here (no cron cadence to have learned from).
|
"prior cycles" injection here (no cron cadence to have learned from).
|
||||||
@@ -13049,12 +13047,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
auditor_slug = "auditor"
|
auditor_slug = "auditor"
|
||||||
if self._is_agent_active(auditor_slug):
|
if self._is_agent_active(auditor_slug):
|
||||||
return
|
return
|
||||||
key = (auditor_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(auditor_slug, task):
|
if await self._pm_respawn_should_gate(auditor_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info("Spawning Auditor for Coroner postmortem", task_id=task_id)
|
logger.info("Spawning Auditor for Coroner postmortem", task_id=task_id)
|
||||||
incident_context = await self._coroner_incident_context(task)
|
incident_context = await self._coroner_incident_context(task)
|
||||||
await self.spawn_agent(
|
await self.spawn_agent(
|
||||||
@@ -13095,7 +13089,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
"already authored" marker shape): no pre-check is needed —
|
"already authored" marker shape): no pre-check is needed —
|
||||||
``propose_campaign`` completes this task atomically, so a successful
|
``propose_campaign`` completes this task atomically, so a successful
|
||||||
call stops it matching the PENDING fetch on the next tick. Reuses the
|
call stops it matching the PENDING fetch on the next tick. Reuses the
|
||||||
same one-shot ``_board_dispatched`` tracker + respawn breaker every
|
respawn breaker every
|
||||||
other board dispatch uses. EVENT-triggered (spec §4): this task only
|
other board dispatch uses. EVENT-triggered (spec §4): this task only
|
||||||
ever exists because a release just published or the CEO called "run
|
ever exists because a release just published or the CEO called "run
|
||||||
now" — there is no LEARN "prior cycles" injection here (no cron
|
now" — there is no LEARN "prior cycles" injection here (no cron
|
||||||
@@ -13108,12 +13102,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
hom_slug = "head-marketing"
|
hom_slug = "head-marketing"
|
||||||
if self._is_agent_active(hom_slug):
|
if self._is_agent_active(hom_slug):
|
||||||
return
|
return
|
||||||
key = (hom_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(hom_slug, task):
|
if await self._pm_respawn_should_gate(hom_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Spawning Head of Marketing for War Room campaign planning",
|
"Spawning Head of Marketing for War Room campaign planning",
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
@@ -13130,7 +13120,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
"""One-shot Product-Owner spawn to author a Spackle gap-fill audit.
|
"""One-shot Product-Owner spawn to author a Spackle gap-fill audit.
|
||||||
|
|
||||||
Mirrors ``_dispatch_pest_control_exploration`` exactly (PO-solo, the
|
Mirrors ``_dispatch_pest_control_exploration`` exactly (PO-solo, the
|
||||||
same one-shot ``_board_dispatched`` tracker + respawn breaker, the
|
respawn breaker the
|
||||||
same "already authored" marker pre-check — ``propose_gap_fill`` marks
|
same "already authored" marker pre-check — ``propose_gap_fill`` marks
|
||||||
it via the ``gap_fill`` marker, not this dispatcher). Unlike Pest
|
it via the ``gap_fill`` marker, not this dispatcher). Unlike Pest
|
||||||
Control there is no server-assembled evidence context — the spec
|
Control there is no server-assembled evidence context — the spec
|
||||||
@@ -13145,12 +13135,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
po_slug = "product-owner"
|
po_slug = "product-owner"
|
||||||
if self._is_agent_active(po_slug):
|
if self._is_agent_active(po_slug):
|
||||||
return
|
return
|
||||||
key = (po_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(po_slug, task):
|
if await self._pm_respawn_should_gate(po_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info("Spawning Product Owner for spackle exploration", task_id=task_id)
|
logger.info("Spawning Product Owner for spackle exploration", task_id=task_id)
|
||||||
prior_context = await self._board_program_prior_context("spackle")
|
prior_context = await self._board_program_prior_context("spackle")
|
||||||
await self.spawn_agent(
|
await self.spawn_agent(
|
||||||
@@ -13166,7 +13152,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
audit.
|
audit.
|
||||||
|
|
||||||
Mirrors ``_dispatch_spackle_exploration`` exactly (HoM-solo, the same
|
Mirrors ``_dispatch_spackle_exploration`` exactly (HoM-solo, the same
|
||||||
one-shot ``_board_dispatched`` tracker + respawn breaker, the same
|
respawn breaker
|
||||||
"already authored" marker pre-check — ``propose_messaging_fixes``
|
"already authored" marker pre-check — ``propose_messaging_fixes``
|
||||||
marks it via the ``messaging_fixes`` marker, not this dispatcher).
|
marks it via the ``messaging_fixes`` marker, not this dispatcher).
|
||||||
Like Spackle there is no server-assembled evidence context — the
|
Like Spackle there is no server-assembled evidence context — the
|
||||||
@@ -13181,12 +13167,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
hom_slug = "head-marketing"
|
hom_slug = "head-marketing"
|
||||||
if self._is_agent_active(hom_slug):
|
if self._is_agent_active(hom_slug):
|
||||||
return
|
return
|
||||||
key = (hom_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(hom_slug, task):
|
if await self._pm_respawn_should_gate(hom_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Spawning Head of Marketing for mirror exploration", task_id=task_id
|
"Spawning Head of Marketing for mirror exploration", task_id=task_id
|
||||||
)
|
)
|
||||||
@@ -13204,7 +13186,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
Dogfood friction audit.
|
Dogfood friction audit.
|
||||||
|
|
||||||
Mirrors ``_dispatch_spackle_exploration`` exactly (PO-solo, the same
|
Mirrors ``_dispatch_spackle_exploration`` exactly (PO-solo, the same
|
||||||
one-shot ``_board_dispatched`` tracker + respawn breaker, the same
|
respawn breaker
|
||||||
"already authored" marker pre-check — ``propose_friction_fixes``
|
"already authored" marker pre-check — ``propose_friction_fixes``
|
||||||
marks it via the ``friction_fixes`` marker, not this dispatcher).
|
marks it via the ``friction_fixes`` marker, not this dispatcher).
|
||||||
There is no server-assembled evidence context — walking the product
|
There is no server-assembled evidence context — walking the product
|
||||||
@@ -13220,12 +13202,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
po_slug = "product-owner"
|
po_slug = "product-owner"
|
||||||
if self._is_agent_active(po_slug):
|
if self._is_agent_active(po_slug):
|
||||||
return
|
return
|
||||||
key = (po_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(po_slug, task):
|
if await self._pm_respawn_should_gate(po_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info("Spawning Product Owner for dogfood exploration", task_id=task_id)
|
logger.info("Spawning Product Owner for dogfood exploration", task_id=task_id)
|
||||||
prior_context = await self._board_program_prior_context("dogfood")
|
prior_context = await self._board_program_prior_context("dogfood")
|
||||||
await self.spawn_agent(
|
await self.spawn_agent(
|
||||||
@@ -13246,19 +13224,14 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
completes this task atomically (it stops matching the PENDING fetch on the
|
completes this task atomically (it stops matching the PENDING fetch on the
|
||||||
next tick) — unlike the roadmap cycle, which stays open across the CEO's
|
next tick) — unlike the roadmap cycle, which stays open across the CEO's
|
||||||
per-item decisions and needs the marker check to avoid re-spawning the PO
|
per-item decisions and needs the marker check to avoid re-spawning the PO
|
||||||
after authoring. Reuses the same one-shot _board_dispatched tracker +
|
after authoring. Reuses the respawn breaker every other board dispatch uses.
|
||||||
respawn breaker every other board dispatch uses.
|
|
||||||
"""
|
"""
|
||||||
task_id = str(task.get("id"))
|
task_id = str(task.get("id"))
|
||||||
hom_slug = "head-marketing"
|
hom_slug = "head-marketing"
|
||||||
if self._is_agent_active(hom_slug):
|
if self._is_agent_active(hom_slug):
|
||||||
return
|
return
|
||||||
key = (hom_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(hom_slug, task):
|
if await self._pm_respawn_should_gate(hom_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Spawning Head of Marketing for feature-spotlight exploration",
|
"Spawning Head of Marketing for feature-spotlight exploration",
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
@@ -13280,19 +13253,14 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
authored" marker pre-check is needed — ``propose_market_brief``
|
authored" marker pre-check is needed — ``propose_market_brief``
|
||||||
completes this task atomically (the x_feature complete-at-propose
|
completes this task atomically (the x_feature complete-at-propose
|
||||||
asymmetry), so it stops matching the PENDING fetch on the next tick.
|
asymmetry), so it stops matching the PENDING fetch on the next tick.
|
||||||
Reuses the same one-shot ``_board_dispatched`` tracker + respawn
|
Reuses respawn breaker every other board dispatch uses.
|
||||||
breaker every other board dispatch uses.
|
|
||||||
"""
|
"""
|
||||||
task_id = str(task.get("id"))
|
task_id = str(task.get("id"))
|
||||||
hom_slug = "head-marketing"
|
hom_slug = "head-marketing"
|
||||||
if self._is_agent_active(hom_slug):
|
if self._is_agent_active(hom_slug):
|
||||||
return
|
return
|
||||||
key = (hom_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(hom_slug, task):
|
if await self._pm_respawn_should_gate(hom_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Spawning Head of Marketing for periscope exploration", task_id=task_id
|
"Spawning Head of Marketing for periscope exploration", task_id=task_id
|
||||||
)
|
)
|
||||||
@@ -13314,19 +13282,15 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
completes this task atomically (the x_feature/periscope complete-at-
|
completes this task atomically (the x_feature/periscope complete-at-
|
||||||
propose asymmetry, multiplied across every materialized reply), so
|
propose asymmetry, multiplied across every materialized reply), so
|
||||||
it stops matching the PENDING fetch on the next tick. Reuses the
|
it stops matching the PENDING fetch on the next tick. Reuses the
|
||||||
same one-shot ``_board_dispatched`` tracker + respawn breaker every
|
respawn breaker every
|
||||||
other board dispatch uses.
|
other board dispatch uses.
|
||||||
"""
|
"""
|
||||||
task_id = str(task.get("id"))
|
task_id = str(task.get("id"))
|
||||||
hom_slug = "head-marketing"
|
hom_slug = "head-marketing"
|
||||||
if self._is_agent_active(hom_slug):
|
if self._is_agent_active(hom_slug):
|
||||||
return
|
return
|
||||||
key = (hom_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(hom_slug, task):
|
if await self._pm_respawn_should_gate(hom_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Spawning Head of Marketing for barfly exploration", task_id=task_id
|
"Spawning Head of Marketing for barfly exploration", task_id=task_id
|
||||||
)
|
)
|
||||||
@@ -13363,8 +13327,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
marker pre-check is needed — ``propose_quality_report`` completes
|
marker pre-check is needed — ``propose_quality_report`` completes
|
||||||
this task atomically (the x_feature/periscope complete-at-propose
|
this task atomically (the x_feature/periscope complete-at-propose
|
||||||
asymmetry), so it stops matching the PENDING fetch on the next tick.
|
asymmetry), so it stops matching the PENDING fetch on the next tick.
|
||||||
Reuses the same one-shot ``_board_dispatched`` tracker + respawn
|
Reuses respawn breaker every other board dispatch uses. The extra step is the
|
||||||
breaker every other board dispatch uses. The extra step is the
|
|
||||||
server-assembled drift evidence (waived-findings trend, open-
|
server-assembled drift evidence (waived-findings trend, open-
|
||||||
findings-by-severity, conventions hotspots, budget snapshot) the
|
findings-by-severity, conventions hotspots, budget snapshot) the
|
||||||
Auditor cannot gather itself — mirrors
|
Auditor cannot gather itself — mirrors
|
||||||
@@ -13374,12 +13337,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
auditor_slug = "auditor"
|
auditor_slug = "auditor"
|
||||||
if self._is_agent_active(auditor_slug):
|
if self._is_agent_active(auditor_slug):
|
||||||
return
|
return
|
||||||
key = (auditor_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(auditor_slug, task):
|
if await self._pm_respawn_should_gate(auditor_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info("Spawning Auditor for sentinel exploration", task_id=task_id)
|
logger.info("Spawning Auditor for sentinel exploration", task_id=task_id)
|
||||||
prior_context = await self._board_program_prior_context("sentinel")
|
prior_context = await self._board_program_prior_context("sentinel")
|
||||||
evidence_context = await self._sentinel_evidence_context()
|
evidence_context = await self._sentinel_evidence_context()
|
||||||
@@ -13416,8 +13375,7 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
marker pre-check is needed — ``propose_editorial_post`` completes
|
marker pre-check is needed — ``propose_editorial_post`` completes
|
||||||
this task atomically (the x_feature/periscope complete-at-propose
|
this task atomically (the x_feature/periscope complete-at-propose
|
||||||
asymmetry), so it stops matching the PENDING fetch on the next tick.
|
asymmetry), so it stops matching the PENDING fetch on the next tick.
|
||||||
Reuses the same one-shot ``_board_dispatched`` tracker + respawn
|
Reuses respawn breaker every other board dispatch uses. The extra step is the
|
||||||
breaker every other board dispatch uses. The extra step is the
|
|
||||||
server-assembled shipped-this-week digest (completed tasks +
|
server-assembled shipped-this-week digest (completed tasks +
|
||||||
CHANGELOG Unreleased bullets) the Head of Marketing cannot gather
|
CHANGELOG Unreleased bullets) the Head of Marketing cannot gather
|
||||||
itself — mirrors ``_dispatch_pest_control_exploration``'s
|
itself — mirrors ``_dispatch_pest_control_exploration``'s
|
||||||
@@ -13427,12 +13385,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
hom_slug = "head-marketing"
|
hom_slug = "head-marketing"
|
||||||
if self._is_agent_active(hom_slug):
|
if self._is_agent_active(hom_slug):
|
||||||
return
|
return
|
||||||
key = (hom_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(hom_slug, task):
|
if await self._pm_respawn_should_gate(hom_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Spawning Head of Marketing for megaphone exploration", task_id=task_id
|
"Spawning Head of Marketing for megaphone exploration", task_id=task_id
|
||||||
)
|
)
|
||||||
@@ -13471,8 +13425,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
marker pre-check is needed — ``propose_playbook_drafts`` completes
|
marker pre-check is needed — ``propose_playbook_drafts`` completes
|
||||||
this task atomically (the x_feature/periscope/sentinel complete-at-
|
this task atomically (the x_feature/periscope/sentinel complete-at-
|
||||||
propose asymmetry), so it stops matching the PENDING fetch on the
|
propose asymmetry), so it stops matching the PENDING fetch on the
|
||||||
next tick. Reuses the same one-shot ``_board_dispatched`` tracker +
|
next tick. Reuses the respawn breaker every other board dispatch
|
||||||
respawn breaker every other board dispatch uses. The extra step is
|
uses. The extra step is
|
||||||
the server-assembled mining context (recurring learning-journal
|
the server-assembled mining context (recurring learning-journal
|
||||||
topics + existing playbook titles) the Auditor cannot gather itself
|
topics + existing playbook titles) the Auditor cannot gather itself
|
||||||
— mirrors ``_dispatch_sentinel_exploration``'s evidence-context
|
— mirrors ``_dispatch_sentinel_exploration``'s evidence-context
|
||||||
@@ -13482,12 +13436,8 @@ Start now: evidence(task_id="{task_id}")
|
|||||||
auditor_slug = "auditor"
|
auditor_slug = "auditor"
|
||||||
if self._is_agent_active(auditor_slug):
|
if self._is_agent_active(auditor_slug):
|
||||||
return
|
return
|
||||||
key = (auditor_slug, task_id)
|
|
||||||
if key in self._board_dispatched:
|
|
||||||
return
|
|
||||||
if await self._pm_respawn_should_gate(auditor_slug, task):
|
if await self._pm_respawn_should_gate(auditor_slug, task):
|
||||||
return
|
return
|
||||||
self._board_dispatched.add(key)
|
|
||||||
logger.info("Spawning Auditor for librarian exploration", task_id=task_id)
|
logger.info("Spawning Auditor for librarian exploration", task_id=task_id)
|
||||||
prior_context = await self._board_program_prior_context("librarian")
|
prior_context = await self._board_program_prior_context("librarian")
|
||||||
mining_context = await self._librarian_mining_context()
|
mining_context = await self._librarian_mining_context()
|
||||||
|
|||||||
@@ -58,9 +58,15 @@ async def test_barfly_dispatch_spawns_only_head_marketing() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_barfly_dispatch_is_one_shot() -> None:
|
async def test_barfly_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-pending exploration must NOT respawn — board roles
|
"""A failed exploration must be retried, not abandoned.
|
||||||
have no progression verb, so a respawn would just loop."""
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _barfly_task()
|
task = _barfly_task()
|
||||||
with (
|
with (
|
||||||
@@ -71,7 +77,10 @@ async def test_barfly_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_barfly_exploration(task)
|
await orch._dispatch_barfly_exploration(task)
|
||||||
await orch._dispatch_barfly_exploration(task)
|
await orch._dispatch_barfly_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -67,8 +67,15 @@ async def test_coroner_dispatch_spawns_only_auditor() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_coroner_dispatch_is_one_shot() -> None:
|
async def test_coroner_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-pending autopsy must NOT respawn."""
|
"""A failed exploration must be retried, not abandoned.
|
||||||
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _coroner_task()
|
task = _coroner_task()
|
||||||
with (
|
with (
|
||||||
@@ -80,7 +87,10 @@ async def test_coroner_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_coroner_exploration(task)
|
await orch._dispatch_coroner_exploration(task)
|
||||||
await orch._dispatch_coroner_exploration(task)
|
await orch._dispatch_coroner_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -61,8 +61,15 @@ async def test_dogfood_dispatch_spawns_only_product_owner() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_dogfood_dispatch_is_one_shot() -> None:
|
async def test_dogfood_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-unauthored, still-pending cycle must NOT respawn."""
|
"""A failed exploration must be retried, not abandoned.
|
||||||
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _dogfood_task()
|
task = _dogfood_task()
|
||||||
with (
|
with (
|
||||||
@@ -73,7 +80,10 @@ async def test_dogfood_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_dogfood_exploration(task)
|
await orch._dispatch_dogfood_exploration(task)
|
||||||
await orch._dispatch_dogfood_exploration(task)
|
await orch._dispatch_dogfood_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -57,9 +57,15 @@ async def test_feature_spotlight_dispatch_spawns_only_head_marketing() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_feature_spotlight_dispatch_is_one_shot() -> None:
|
async def test_feature_spotlight_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-pending exploration must NOT respawn — board roles
|
"""A failed exploration must be retried, not abandoned.
|
||||||
have no progression verb, so a respawn would just loop."""
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _feature_task()
|
task = _feature_task()
|
||||||
with (
|
with (
|
||||||
@@ -70,7 +76,10 @@ async def test_feature_spotlight_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_feature_spotlight_exploration(task)
|
await orch._dispatch_feature_spotlight_exploration(task)
|
||||||
await orch._dispatch_feature_spotlight_exploration(task)
|
await orch._dispatch_feature_spotlight_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -59,9 +59,15 @@ async def test_librarian_dispatch_spawns_only_auditor() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_librarian_dispatch_is_one_shot() -> None:
|
async def test_librarian_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-pending mining task must NOT respawn — board roles
|
"""A failed exploration must be retried, not abandoned.
|
||||||
have no progression verb, so a respawn would just loop."""
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _librarian_task()
|
task = _librarian_task()
|
||||||
with (
|
with (
|
||||||
@@ -72,7 +78,10 @@ async def test_librarian_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_librarian_exploration(task)
|
await orch._dispatch_librarian_exploration(task)
|
||||||
await orch._dispatch_librarian_exploration(task)
|
await orch._dispatch_librarian_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -58,9 +58,15 @@ async def test_megaphone_dispatch_spawns_only_head_marketing() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_megaphone_dispatch_is_one_shot() -> None:
|
async def test_megaphone_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-pending exploration must NOT respawn — board roles
|
"""A failed exploration must be retried, not abandoned.
|
||||||
have no progression verb, so a respawn would just loop."""
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _megaphone_task()
|
task = _megaphone_task()
|
||||||
with (
|
with (
|
||||||
@@ -71,7 +77,10 @@ async def test_megaphone_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_megaphone_exploration(task)
|
await orch._dispatch_megaphone_exploration(task)
|
||||||
await orch._dispatch_megaphone_exploration(task)
|
await orch._dispatch_megaphone_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -57,8 +57,15 @@ async def test_mirror_dispatch_spawns_only_head_of_marketing() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_mirror_dispatch_is_one_shot() -> None:
|
async def test_mirror_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-unauthored, still-pending cycle must NOT respawn."""
|
"""A failed exploration must be retried, not abandoned.
|
||||||
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _mirror_task()
|
task = _mirror_task()
|
||||||
with (
|
with (
|
||||||
@@ -69,7 +76,10 @@ async def test_mirror_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_mirror_exploration(task)
|
await orch._dispatch_mirror_exploration(task)
|
||||||
await orch._dispatch_mirror_exploration(task)
|
await orch._dispatch_mirror_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -59,9 +59,15 @@ async def test_periscope_dispatch_spawns_only_head_marketing() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_periscope_dispatch_is_one_shot() -> None:
|
async def test_periscope_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-pending exploration must NOT respawn — board roles
|
"""A failed exploration must be retried, not abandoned.
|
||||||
have no progression verb, so a respawn would just loop."""
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _periscope_task()
|
task = _periscope_task()
|
||||||
with (
|
with (
|
||||||
@@ -72,7 +78,10 @@ async def test_periscope_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_periscope_exploration(task)
|
await orch._dispatch_periscope_exploration(task)
|
||||||
await orch._dispatch_periscope_exploration(task)
|
await orch._dispatch_periscope_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -57,8 +57,15 @@ async def test_pest_control_dispatch_spawns_only_product_owner() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pest_control_dispatch_is_one_shot() -> None:
|
async def test_pest_control_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-unauthored, still-pending cycle must NOT respawn."""
|
"""A failed exploration must be retried, not abandoned.
|
||||||
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _pest_control_task()
|
task = _pest_control_task()
|
||||||
with (
|
with (
|
||||||
@@ -69,7 +76,10 @@ async def test_pest_control_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_pest_control_exploration(task)
|
await orch._dispatch_pest_control_exploration(task)
|
||||||
await orch._dispatch_pest_control_exploration(task)
|
await orch._dispatch_pest_control_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -57,9 +57,15 @@ async def test_roadmap_dispatch_spawns_only_product_owner() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_roadmap_dispatch_is_one_shot() -> None:
|
async def test_roadmap_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-unauthored, still-pending cycle must NOT respawn —
|
"""A failed exploration must be retried, not abandoned.
|
||||||
board roles have no progression verb, so a respawn would just loop."""
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _roadmap_task()
|
task = _roadmap_task()
|
||||||
with (
|
with (
|
||||||
@@ -70,7 +76,10 @@ async def test_roadmap_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_roadmap_exploration(task)
|
await orch._dispatch_roadmap_exploration(task)
|
||||||
await orch._dispatch_roadmap_exploration(task)
|
await orch._dispatch_roadmap_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -57,8 +57,15 @@ async def test_scales_dispatch_spawns_only_product_owner() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_scales_dispatch_is_one_shot() -> None:
|
async def test_scales_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-unauthored, still-pending cycle must NOT respawn."""
|
"""A failed exploration must be retried, not abandoned.
|
||||||
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _scales_task()
|
task = _scales_task()
|
||||||
with (
|
with (
|
||||||
@@ -69,7 +76,10 @@ async def test_scales_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_scales_exploration(task)
|
await orch._dispatch_scales_exploration(task)
|
||||||
await orch._dispatch_scales_exploration(task)
|
await orch._dispatch_scales_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -59,9 +59,15 @@ async def test_sentinel_dispatch_spawns_only_auditor() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_sentinel_dispatch_is_one_shot() -> None:
|
async def test_sentinel_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-pending exploration must NOT respawn — board roles
|
"""A failed exploration must be retried, not abandoned.
|
||||||
have no progression verb, so a respawn would just loop."""
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _sentinel_task()
|
task = _sentinel_task()
|
||||||
with (
|
with (
|
||||||
@@ -72,7 +78,10 @@ async def test_sentinel_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_sentinel_exploration(task)
|
await orch._dispatch_sentinel_exploration(task)
|
||||||
await orch._dispatch_sentinel_exploration(task)
|
await orch._dispatch_sentinel_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -57,8 +57,15 @@ async def test_spackle_dispatch_spawns_only_product_owner() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_spackle_dispatch_is_one_shot() -> None:
|
async def test_spackle_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-unauthored, still-pending cycle must NOT respawn."""
|
"""A failed exploration must be retried, not abandoned.
|
||||||
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _spackle_task()
|
task = _spackle_task()
|
||||||
with (
|
with (
|
||||||
@@ -69,7 +76,10 @@ async def test_spackle_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_spackle_exploration(task)
|
await orch._dispatch_spackle_exploration(task)
|
||||||
await orch._dispatch_spackle_exploration(task)
|
await orch._dispatch_spackle_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -67,8 +67,15 @@ async def test_war_room_dispatch_spawns_only_hom() -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_war_room_dispatch_is_one_shot() -> None:
|
async def test_war_room_dispatch_retries_until_breaker() -> None:
|
||||||
"""Re-ticking a still-pending campaign-planning task must NOT respawn."""
|
"""A failed exploration must be retried, not abandoned.
|
||||||
|
|
||||||
|
The explorer has a progression verb (``propose_*``), so a respawn CAN
|
||||||
|
advance the task — unlike the two-reviewer review pass this guard was
|
||||||
|
originally written for. Bounding belongs to
|
||||||
|
``_pm_respawn_should_gate`` (DB-persisted, reset by a status change),
|
||||||
|
not to a never-expiring in-memory set.
|
||||||
|
"""
|
||||||
orch = _make_orch()
|
orch = _make_orch()
|
||||||
task = _war_room_task()
|
task = _war_room_task()
|
||||||
with (
|
with (
|
||||||
@@ -79,7 +86,10 @@ async def test_war_room_dispatch_is_one_shot() -> None:
|
|||||||
await orch._dispatch_war_room_exploration(task)
|
await orch._dispatch_war_room_exploration(task)
|
||||||
await orch._dispatch_war_room_exploration(task)
|
await orch._dispatch_war_room_exploration(task)
|
||||||
|
|
||||||
spawn.assert_awaited_once()
|
ticks = 2
|
||||||
|
assert spawn.await_count == ticks, (
|
||||||
|
"a second tick must re-attempt a failed exploration"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user