now = $now ?? static fn (): DateTimeImmutable => new DateTimeImmutable( 'now' ); } /** * Ride the hourly expiry sweep, after the backlog check (priority 20). */ public function register(): void { add_action( ExpiryCron::HOOK, array( $this, 'sweep' ), 25 ); } /** * The cron callback. */ public function sweep(): void { $this->run(); } /** * Announce every hold that has waited too long for its AI review. * * @return int How many were announced or marked. */ public function run(): int { $cutoff = ( $this->now )()->getTimestamp() - self::AFTER; $count = 0; foreach ( $this->pending->list_awaiting_ai( self::LIMIT ) as $listed ) { if ( $listed['held_at']->getTimestamp() > $cutoff ) { continue; } $hold = $listed['pending']; $entry = $this->ledger->find( $hold->ledger_id ); $result = $entry->result ?? array(); if ( null === $entry || true !== ( $result['awaits_ai'] ?? null ) || isset( $result['ai_verdict'] ) ) { continue; } // Marked first, so a review that arrives late finds the hold no // longer waiting and leaves it to the human now told about it. $this->ledger->update_result( $entry->id, array_merge( $result, array( 'awaits_ai' => false, 'review_timed_out' => true, ) ) ); ++$count; $agent = $this->agents->find_by_id( $entry->agent_id ); if ( null !== $agent ) { // Documented in InterceptionPipeline: a human must act. do_action( 'adaf_action_pending', $hold->id, $agent, $entry->action( $hold->stored_input ), $entry->id, false ); } } return $count; } }