fbf15bb5de
Co-authored-by: nova28 <17953305+nova28@users.noreply.github.com> Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
6.5 KiB
6.5 KiB
id, title, status, wave, depends_on, plan, requirements, acceptance_criteria, system_design
| id | title | status | wave | depends_on | plan | requirements | acceptance_criteria | system_design | |||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-assignment-wake-rate-limit | Bound agent-initiated assignment wakes per task | done | 1 | plan.md |
|
|
|
Task 01: Bound agent-initiated assignment wakes per task
Summary
Add one admission gate to the Office run queue that refuses an agent-initiated
task_assigned wake once a task has already produced N of them inside a
rolling window W. The gate is the last check before insertion, so "admitted"
means "a row was written" and the count needs no state of its own.
Scope
- A window-count repository read over the stored
runsrows, filtered byreason, the payload'stask_idandactor_type, andrequested_at, usingdialect.JSONExtractand the string-type guardCoalesceRunalready needs. - The incoming wake's
actor_typeandtask_idread in Go, by unmarshalling the payloadqueueRunwas handed, the waytaskIDFromPayloaddoes.dialect.JSONExtractis SQL over a column and does not apply to a wake that is not a row yet; the two reads are separate. - The gate in
SchedulerService.queueRun, positioned afterCheckIdempotencyKeyandCoalesceRunand beforeCreateRun. That position binds the task-mutation producer only (AC-OFFICE-ASSIGN-RATE-001.12); no othertask_assignedproducer routes through this function. RunContext.ActorTypereaching the payload for the mutation producer's agent-actor wakes, pinned by a test (AC-OFFICE-ASSIGN-RATE-001.13). A wake carrying noactor_typeis out of scope: admit, no allowance consumed, no counter moved.N = 5andW = 10 * time.Minutedeclared once besideCoalesceWindowSecondsandIdempotencyWindowHours.- A new
QueueOutcomevalue added to both declarations (internal/runs/serviceandinternal/workflow/engine/adapters.go). - A reporting helper beside
ReportWindowedDedup/ReportDurableDedup, a newexpvar.Mapbesideoffice_run_dedup_total, and a Warn log carrying task id, assignee profile id, acting agent id, observed count and allowance.
Exclusions
- No change to dedup key construction, the coalescing predicate, the 24-hour lookback, provenance classification, or budget admission.
- No change to
SetTaskAssigneeAsAgent's permission check or to the mutation itself: a refused wake still persists the assignee, bumpsassignment_generation, publishes the task update, and interrupts a displaced assignee's session. - No bound on the other four
task_assignedproducers: the task-lifecycle event subscriber (service/event_subscribers.go), onboarding, the recovery sweep, and the orchestrator's workflow auto-start. None carries an actor, and a per-task allowance cannot bind a producer that wakes a task just created, onboarded or swept. Do not "fix" this by giving one of them an actor. - No ordering or tiebreak logic.
AC-OFFICE-ASSIGN-RATE-001.10makes the decision order-independent; noORDER BYbelongs in the window count. - No frontend, no migration, no new configuration surface.
Implementation acceptance conditions
Nagent-actor assignment wakes for one task admit and the next refuses, writing no row and returning the new outcome; auser-actor wake and an empty-callerAgentIDcall are never refused, and neither is a wake carrying noactor_typeat all.- A wake merged by coalescing or suppressed by the idempotency lookup neither consumes the allowance nor can be refused by it; a refused wake does not consume it either.
- A failing count read and an unattributable task both admit, each
incrementing the counter under its own
reasonlabel, with no identifier used as a label. Unattributable covers an absent, null or emptytask_idand a present-but-non-string one, identically; an unparseable payload is not unattributable — it fails the actor step first and leaves scope with no counter moved. - A test enumerates the
task_assignedproducers and asserts the actor type each emits, pinningAC-OFFICE-ASSIGN-RATE-001.12's invariant that no other producer setsactor_typeagent. - A mixed-actor coalesce is covered in both directions and asserts the count the
gate then observes, which is the accepted off-by-one in the requirement's
## Accepted consequencerather than an exact count.
Verification
cd apps/backend && go test ./internal/office/scheduler ./internal/office/service \
./internal/runs/service ./internal/workflow/engine
cd apps/backend && make lint
Likely files
apps/backend/internal/office/scheduler/run.goapps/backend/internal/office/scheduler/reactivity.goapps/backend/internal/runs/service/dedup.goapps/backend/internal/runs/service/metrics_vars.goapps/backend/internal/workflow/engine/adapters.goapps/backend/internal/runs/repository/**(window-count query, SQLite and PostgreSQL twins)
Risks
- The
QueueOutcomeenum is declared twice and must stay in sync; adding the value to one declaration only breaks the engine adapter's reporting. - A hand-written
json_extractdiverges between SQLite and PostgreSQL. Usedialect.JSONExtractand guard the JSON string type, or a numerictask_idin an older payload silently joins another task's allowance. - Tests must inject time (
synctest), nevertime.Sleep. CoalesceRunoverwrites the surviving row's payload and does not match onactor_type, so a mixed-actor merge relabels the actor the count reads. This is an accepted consequence, not a bug to fix here: do not addactor_typeto the coalescing predicate, and do not write a test that asserts an exact count across such a merge.