fix: disable agent dry-run without executor
This commit is contained in:
@@ -21,7 +21,7 @@ import (
|
||||
func main() {
|
||||
workerID := flag.String("worker-id", "agent-worker-1", "durable worker identity")
|
||||
staleAfter := flag.Duration("stale-after", 10*time.Minute, "age after which a claimed item is recovered")
|
||||
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "dry-run lease duration")
|
||||
leaseTTL := flag.Duration("lease-ttl", 15*time.Minute, "executor lease duration")
|
||||
pollInterval := flag.Duration("poll-interval", 5*time.Second, "database-owned idle wake-up interval")
|
||||
once := flag.Bool("once", false, "run one cycle and exit")
|
||||
flag.Parse()
|
||||
@@ -45,14 +45,14 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "agent execution bridge is not configured; using legacy dry-run dispatch")
|
||||
fmt.Fprintln(os.Stderr, "agent execution bridge is not configured; dispatch claims are disabled; recovery-only mode")
|
||||
}
|
||||
ctx := context.Background()
|
||||
for {
|
||||
var result application.WorkerCycleResult
|
||||
var cycleErr error
|
||||
if executor == nil {
|
||||
result, cycleErr = application.ExecuteWorkerCycle(ctx, repository, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
|
||||
result, cycleErr = application.RecoverOnlyWorkerCycle(ctx, repository, time.Now().UTC(), *staleAfter)
|
||||
} else {
|
||||
result, cycleErr = application.ExecuteWorkerCycleWithExecutor(ctx, repository, executor, *workerID, work.AgentFencingToken(1), time.Now().UTC(), *staleAfter, *leaseTTL)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"task_file": "dev/tasks/DEV-000147-model-fleet-operations-ui.json",
|
||||
"task_id": "DEV-000147",
|
||||
"task_file": "dev/tasks/DEV-000148-agent-worker-no-dry-run.json",
|
||||
"task_id": "DEV-000148",
|
||||
"task_revision": 1
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"task_id": "DEV-000148",
|
||||
"task_revision": 1,
|
||||
"architecture_revision": "ADR-0018",
|
||||
"base_repo_revision": "7030a51b69a4d7345c5475d2fcced0e6d0441916",
|
||||
"risk_tier": "E",
|
||||
"intent": "Prevent agent-worker from claiming or completing dispatches without a configured real executor while preserving durable stale-lease recovery.",
|
||||
"requirement_ids": ["WP-301", "WP-302", "INV-AUTH-001", "INV-CONT-001"],
|
||||
"allowed_change_scope": {"modules": ["work", "agent-worker"], "paths": ["cmd/agent-worker/main.go", "modules/work/application/worker.go", "modules/work/application/worker_test.go", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000148-agent-worker-no-dry-run.json"]},
|
||||
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", ".gitea/**", "database/**", "contracts/**", "adapters/**", "deploy/**"]},
|
||||
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||
"contracts": {"consume": ["WorkerStore", "AgentExecutor"], "may_change": []},
|
||||
"semantic_owner": "work",
|
||||
"canonical_state": ["dispatch queue claims and stale lease recovery"],
|
||||
"state_transitions": ["executor absent -> recovery-only; no new dispatch claim"],
|
||||
"side_effects_and_privileges": ["work-owner stale claim recovery only when executor is absent"],
|
||||
"persistence_and_migrations": [],
|
||||
"failure_retry_restart": ["worker remains restartable and resumes recovery-only mode until bridge is configured"],
|
||||
"assumptions": ["AGENT_EXECUTION_BRIDGE_URL is the only production activation path for external execution"],
|
||||
"acceptance_scenarios": ["missing bridge never claims a queued dispatch", "missing bridge never marks work completed", "stale claims are still recovered", "configured bridge path remains unchanged"],
|
||||
"required_tests": ["go test ./...", "go vet ./...", "preflight", "check-diff", "check-architecture", "independent", "git diff --check"],
|
||||
"terminal_gap_states": ["BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||
}
|
||||
@@ -15,6 +15,14 @@ type WorkerCycleResult struct {
|
||||
Idle bool
|
||||
}
|
||||
|
||||
// RecoverOnlyWorkerCycle keeps durable recovery alive while the real executor
|
||||
// capability is absent. It intentionally does not claim a dispatch: a missing
|
||||
// executor must never be represented as successful work.
|
||||
func RecoverOnlyWorkerCycle(ctx context.Context, store ports.WorkerStore, now time.Time, staleAfter time.Duration) (WorkerCycleResult, error) {
|
||||
recovered, err := store.RecoverStaleClaims(ctx, now.UnixMilli(), staleAfter.Milliseconds())
|
||||
return WorkerCycleResult{RecoveredClaims: recovered, Idle: err == nil}, err
|
||||
}
|
||||
|
||||
func ExecuteWorkerCycle(ctx context.Context, store ports.WorkerStore, workerID string, token work.AgentFencingToken, now time.Time, staleAfter, leaseTTL time.Duration) (WorkerCycleResult, error) {
|
||||
recovered, err := store.RecoverStaleClaims(ctx, now.UnixMilli(), staleAfter.Milliseconds())
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/work"
|
||||
)
|
||||
|
||||
type recoveryOnlyStore struct {
|
||||
recovered int64
|
||||
claimed bool
|
||||
}
|
||||
|
||||
func (s *recoveryOnlyStore) RecoverStaleClaims(context.Context, int64, int64) (int64, error) {
|
||||
return s.recovered, nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) ClaimNextDispatch(context.Context, string, int64) (work.AgentDispatchQueueItem, error) {
|
||||
s.claimed = true
|
||||
return work.AgentDispatchQueueItem{}, nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) LoadDispatchIntent(context.Context, string) (work.AgentDispatchIntent, error) {
|
||||
return work.AgentDispatchIntent{}, nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) CompleteDispatch(context.Context, string, string, work.AgentDispatchQueueState, int64) error {
|
||||
return nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) SaveDispatchIntent(context.Context, work.AgentDispatchIntent) error {
|
||||
return nil
|
||||
}
|
||||
func (s *recoveryOnlyStore) SaveAgentLease(context.Context, work.AgentLease) error { return nil }
|
||||
func (s *recoveryOnlyStore) AcceptAgentCallback(context.Context, work.AgentCallback) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRecoverOnlyWorkerCycleNeverClaimsDispatch(t *testing.T) {
|
||||
store := &recoveryOnlyStore{recovered: 2}
|
||||
result, err := RecoverOnlyWorkerCycle(context.Background(), store, time.UnixMilli(1_700_000_000_000), time.Minute)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.RecoveredClaims != 2 || !result.Idle || store.claimed {
|
||||
t.Fatalf("unexpected recovery-only result: %+v claimed=%t", result, store.claimed)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user