Skip to content

PendingActionStore: observer registry + WordPress action dispatch adapter #1928

@chubes4

Description

@chubes4

Background

`agents-api` is gaining a new primitive: `WP_Agent_Pending_Action_Observer` (Automattic/agents-api#128). It defines a lifecycle contract — `on_stored`, `on_resolved`, `on_expired` — that any Store implementation can invoke when pending-action state changes.

DM's `PendingActionStore` is the canonical Store implementation. This issue wires DM up to:

  1. Maintain a registry of observers that consumers can register against.
  2. Invoke observers at the right lifecycle points inside DM's store/resolve/expire methods.
  3. Ship a default `WordPressActionDispatchObserver` that translates each lifecycle call into a `do_action()`, so existing WordPress-shaped consumers can hook into pending-action events with native `add_action()`.

Together, this gives Studio (and any future consumer) two equally valid integration paths:

  • Implement `WP_Agent_Pending_Action_Observer` for object-oriented consumers and observability tooling.
  • Use `add_action( 'datamachine_pending_action_stored', ... )` for plugin-style consumers.

Scope

Observer registry

Add a small registry (probably static class `PendingActionObservers` under `inc/Engine/AI/Actions/`) with:

  • `register( WP_Agent_Pending_Action_Observer $observer ): void`
  • `unregister( WP_Agent_Pending_Action_Observer $observer ): void`
  • `list(): array<WP_Agent_Pending_Action_Observer>`
  • Internal `dispatch_stored( $action )`, `dispatch_resolved( $action, $decision, $resolver )`, `dispatch_expired( $action )` that loop the registry, call the matching method, and swallow exceptions per observer.

Each dispatch wraps individual observer calls in try/catch so a misbehaving observer can't break the store. Errors get logged via the existing DM logger.

Wire up dispatch points in PendingActionStore

  • `store()` — after successful row insert, call `PendingActionObservers::dispatch_stored( $action )`.
  • `record_resolution()` — after successful audit row update, call `PendingActionObservers::dispatch_resolved( $action, $decision, $resolver )`.
  • `expire_due_actions()` — for each expired action, call `PendingActionObservers::dispatch_expired( $action )`.

Construct `WP_Agent_Pending_Action` from the row payload before dispatching when the methods don't already have one in scope.

Default WordPress action adapter

Ship a default observer `WordPressActionDispatchObserver` registered automatically on `plugins_loaded` or DM bootstrap. Each method does:

```php
public function on_stored( WP_Agent_Pending_Action $action ): void {
do_action( 'datamachine_pending_action_stored', $action );
}

public function on_resolved( ... ): void {
do_action( 'datamachine_pending_action_resolved', $action, $decision, $resolver );
}

public function on_expired( WP_Agent_Pending_Action $action ): void {
do_action( 'datamachine_pending_action_expired', $action );
}
```

This means consumers can do:

```php
add_action( 'datamachine_pending_action_stored', function ( $action ) {
if ( $action->kind() !== 'studio_socials_publish' ) return;
studio_send_approval_email( $action );
} );
```

Constraints

  • Observer dispatch happens after the durable write succeeds — never on a failure path. This keeps observers honest about lifecycle truth.
  • Observers MUST NOT block. Long-running work (sending email, calling external APIs) must be deferred to Action Scheduler from inside the hook.
  • Multiple observers can be registered — order is undefined.
  • Conventional commits: `feat: pending-action observer registry + WP action adapter`.

Acceptance

  • New file `inc/Engine/AI/Actions/PendingActionObservers.php` (registry + dispatch).
  • New file `inc/Engine/AI/Actions/WordPressActionDispatchObserver.php` (default adapter).
  • `PendingActionStore::store()`, `record_resolution()`, `expire_due_actions()` invoke registry dispatch.
  • Default WP action adapter registered on bootstrap.
  • Three new WP actions documented: `datamachine_pending_action_stored`, `_resolved`, `_expired`.
  • Failing observer doesn't break the store — exceptions caught and logged.
  • No regression to existing PendingActionStore consumers.

Depends on

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions