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:
- Maintain a registry of observers that consumers can register against.
- Invoke observers at the right lifecycle points inside DM's store/resolve/expire methods.
- 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
Depends on
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:
Together, this gives Studio (and any future consumer) two equally valid integration paths:
Scope
Observer registry
Add a small registry (probably static class `PendingActionObservers` under `inc/Engine/AI/Actions/`) with:
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
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
Acceptance
Depends on