Files
multica/server/internal/dispatch/reason.go
J 9838751c91 fix(comments): fail-closed active-task check, never fake deferred (MUL-4525)
Elon round-4 must-fix: hasActiveTaskForIssueAndAgent swallowed DB errors by
returning true, so both call sites could turn a query failure into a
success-shaped `deferred/already_active` — a silent false success, exactly what
this issue forbids for admission-query failures.

- hasActiveTaskForIssueAndAgent now returns (bool, error).
- Two pure decision helpers govern the branches, fail closed on error:
  - decidePostMergeMiss: on query error, do NOT enqueue a fresh task (duplicate
    concurrent-run risk) AND report non-success blocked/internal_error; a
    confirmed active task defers; a confirmed-none enqueues fresh.
  - decideSuppressedLeaderOutcome: on query error, blocked/internal_error; a
    confirmed active run defers; else self_trigger_suppressed. Never a fabricated
    deferred.
- Renamed reason already_handled -> self_trigger_suppressed (Elon non-blocking
  note): the old name implied the new comment was already processed, but the
  real meaning is a suppressed self-trigger.

Deterministic unit tests cover the query-failure branch at both call sites
(no fresh enqueue, non-success outcome) — a real DB fault can't be forced
through valid handler inputs, and the decision is what governs the behavior.
Backend handler+service suites and go vet green.

Co-authored-by: multica-agent <github@multica.ai>
2026-07-15 01:41:03 +08:00

49 lines
2.6 KiB
Go

// Package dispatch holds the canonical, cross-layer vocabulary for execution
// admission outcomes (MUL-4525). It is a leaf package (no internal deps) so both
// the service layer — which MAKES the admission/skip decision and therefore owns
// the reason at its source — and the handler layer — which serializes it to the
// wire — share one enum and can never drift.
//
// A ReasonCode is decided at the branch that blocks/skips a run and carried
// through to the response verbatim; it is never reverse-engineered from a
// human-readable failure string. Codes are stable, localizable by clients, and
// enumeration-safe: a code never reveals whether a private agent exists, its
// name, or its owner.
package dispatch
// ReasonCode is a stable, client-localizable admission/dispatch reason.
type ReasonCode string
const (
// ReasonQueued / ReasonCoalesced / ReasonDeferred are the success-path codes.
ReasonQueued ReasonCode = "queued"
ReasonCoalesced ReasonCode = "coalesced"
ReasonDeferred ReasonCode = "deferred"
// ReasonInvocationNotAllowed: the acting principal may not trigger this
// target under the invocation-permission model. Deliberately generic — it
// does not distinguish "target is private" from "target does not exist".
ReasonInvocationNotAllowed ReasonCode = "invocation_not_allowed"
// ReasonTargetUnavailable: the target cannot run (archived agent, deleted /
// archived squad, unresolvable leader, or no assignee).
ReasonTargetUnavailable ReasonCode = "target_unavailable"
// ReasonRuntimeOffline: the target is permitted but its runtime is not bound
// / not online at dispatch time.
ReasonRuntimeOffline ReasonCode = "runtime_offline"
// ReasonAttributionBlocked: a fail-closed workspace could not resolve a
// responsible human for the run, so it was refused.
ReasonAttributionBlocked ReasonCode = "attribution_blocked"
// ReasonAlreadyActive: a run is already active/pending for this target and
// this trigger did not coalesce.
ReasonAlreadyActive ReasonCode = "already_active"
// ReasonSelfTriggerSuppressed: the target was intentionally not (re-)triggered
// because doing so would be a self-trigger the guard suppresses, and no active
// run remains to cover it — e.g. a squad leader's own @mention of its squad
// whose latest task is already terminal. Not a permission block, but NOT
// success: nothing new runs. (Named to avoid implying the NEW comment was
// already processed.)
ReasonSelfTriggerSuppressed ReasonCode = "self_trigger_suppressed"
// ReasonInternalError: an unexpected server error prevented a clean decision.
ReasonInternalError ReasonCode = "internal_error"
)