Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agents-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@
require_once AGENTS_API_PATH . 'src/Routines/class-wp-agent-routine-registry.php';
require_once AGENTS_API_PATH . 'src/Routines/class-wp-agent-routine-action-scheduler-bridge.php';
require_once AGENTS_API_PATH . 'src/Routines/register-routines.php';
require_once AGENTS_API_PATH . 'src/Routines/register-routine-abilities.php';
require_once AGENTS_API_PATH . 'src/Routines/register-routine-bridge-sync.php';
require_once AGENTS_API_PATH . 'src/Routines/register-action-scheduler-listener.php';
require_once AGENTS_API_PATH . 'src/Triggers/class-wp-agent-event-trigger.php';
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"php tests/workflow-lifecycle-smoke.php",
"php tests/agents-workflow-ability-smoke.php",
"php tests/routine-smoke.php",
"php tests/routines-durability-smoke.php",
"php tests/event-trigger-smoke.php",
"php tests/subagents-smoke.php",
"php tests/access-decision-filter-smoke.php",
Expand Down
23 changes: 23 additions & 0 deletions docs/channels-workflows-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,29 @@ Optional fields include `label`, `prompt`, `session_id`, and `meta`. When `sessi

Action Scheduler bridges and listeners are optional operational adapters. The substrate detects Action Scheduler at runtime and no-ops cleanly when absent; `composer.json` suggests `woocommerce/action-scheduler` for scheduled workflow/routine execution.

### Routine generation fencing

Every `WP_Agent_Routine_Action_Scheduler_Bridge::register()` mints a schedule generation (`wp_generate_uuid4()`) and persists it in the non-autoloaded `agents_routine_generation_<routine_id>` option. Scheduled action args stay purely logical — `array( 'routine_id' => ... )` — so Action Scheduler's exact-match queries (`as_unschedule_all_actions`, `as_next_scheduled_action`) keep working and `register()` stays O(1) regardless of how many routines exist. The generation is recorded **per stored action**, keyed by action id, in `agents_routine_action_generation_<action_id>`.

Two Action Scheduler hooks carry the mechanism:

- `action_scheduler_stored_action` fires after every insert — the initial schedule and each recurrence successor AS spawns. The bridge stamps the routine's *current* generation onto the new action id. A successor stored after re-registration therefore carries the new generation and is live; one stored by a late-finishing old-chain action still carries the old generation and is stale.
- `action_scheduler_before_execute` fires before the queue runner re-checks that the action is still pending. When the stamp no longer matches the routine's current generation the bridge cancels the action (firing `agents_routine_action_fenced( $routine_id, $stamped, $action_id )` for observability); the runner's own status check then ignores it and no recurrence successor is spawned. Unstamped (pre-fencing) actions are allowed to run.

`cancel_action_by_id()` deletes the per-action stamp alongside the cancel. `WP_Agent_Routine_Registry::current_generation( $id )` exposes the persisted routine generation; `unregister()` deletes it.

### Routine stagger

Routines registered with the same interval would all fire in the same second. `WP_Agent_Routine` accepts `stagger => bool|int` (default `true` for interval routines, `false` for cron expressions, where the expression already *is* the slot; an int is an explicit max window in seconds). `WP_Agent_Routine::stagger_offset()` computes `crc32( 'agents_routine_stagger_' . $id ) % min( interval, max_window )`, capped by `WP_Agent_Routine::MAX_STAGGER_SECONDS` (one hour). The bridge adds the offset to the first-run timestamp. The offset depends only on the routine id, so re-registration always lands the routine back in the same slot.

### Routine reconcile

`WP_Agent_Routine_Registry::reconcile( array $opts = [] )` repairs drift between the registry and the Action Scheduler store. For every registered, non-paused routine it checks pending-action coverage by logical identity and enqueues a fresh schedule when coverage is missing; pending routine actions whose logical `routine_id` is not registered (or is durably paused) are unscheduled as orphans. It returns `array( 'enqueued' => [ids], 'removed' => [ids], 'unchanged' => [ids], 'errors' => [id => message] )`; `$opts['dry_run']` reports the same shape without writing. The run is serialized through an `add_option()` compare-and-set lock (`agents_routine_reconcile_lock`); a lock older than five minutes is treated as stale and taken over, and the lock is always released in a `finally`.

Pause state is durable: the bridge maintains the `agents_routine_paused` option so reconcile can distinguish "unscheduled on purpose" from "missing by drift" across requests, even though the registry itself stays in-memory.

The `agents/reconcile-routines` ability (`show_in_rest: true`, annotations `destructive: true, idempotent: true`) exposes the same operation to ability consumers: input `{ dry_run?: bool }`, output the reconcile report, permission `current_user_can( 'manage_options' )` filterable via `agents_reconcile_routines_permission`.

## Transcripts and approvals

Transcript contracts live in `src/Transcripts/` and runtime persister contracts live in `src/Runtime/`:
Expand Down
Loading