14 KiB
WP Agentic Writer Sixth Retrace Audit
Status: COMPLETE / RETRACED
Completion marker: 2026-05-25
Follow-up report: docs/architecture/PLUGIN_AUDIT_RETRACE_SEVENTH_PASS_2026-05-25.md
Audit date: 2026-05-25
Baseline retraced: docs/architecture/PLUGIN_AUDIT_RETRACE_FIFTH_PASS_2026-05-25.md
Scope: sixth pass after fifth-retrace implementation, covering REST authorization, UI/UX risk, conversation context/history, provider transparency, cost attribution, model defaults, and release readiness.
Executive Summary
The fifth-pass implementation closed the main P0 defects that were causing the "fix A, lose B" cycle:
- Permission checks for the previously flagged post-scoped handlers now happen before post config, meta, content, provider, streaming, or cost work.
- Utility routes that accept
postIdfor cost/context attribution now validateedit_postbefore using that post id. - Legacy chat migration now has stronger migrate-on-read behavior and deletes old
_wpaw_chat_historyafter migration. - PHP and JavaScript syntax checks pass.
I did not find a new P0 blocker in this retrace. The remaining gaps are narrower and mostly about consistency contracts: provider metadata is still only complete in chat, migrate-on-read can still return a stale session_id, legacy chat history is still exposed through a deprecated route, model defaults remain fragmented, and browser-level UI compatibility has not been verified.
Verification Performed
- PHP syntax check across plugin PHP files: passed.
node -c assets/js/sidebar.js: passed.node -c assets/js/settings-v2.js: passed.- Static retrace of fifth-pass findings against current code.
- Static sweep of provider metadata, context migration, cost tracker, model defaults, and legacy chat history surfaces.
- No live WordPress editor browser workflow was run in this pass.
Fifth-Pass Status Trace
| Fifth-pass item | Current status | Evidence |
|---|---|---|
handle_revise_plan() permission ordering |
Fixed | postId is extracted and checked before post config/meta/provider work in includes/class-gutenberg-sidebar.php:2023-2057. |
handle_block_refine() permission ordering |
Fixed | edit_post is checked before post config/provider work in includes/class-gutenberg-sidebar.php:4207-4236. |
handle_refine_from_chat() permission ordering |
Fixed | edit_post is checked before post config/streaming work in includes/class-gutenberg-sidebar.php:4863-4893. |
handle_generate_meta() permission ordering |
Fixed | edit_post is checked before get_post() in includes/class-gutenberg-sidebar.php:6085-6108. |
handle_check_clarity() permission ordering |
Fixed | edit_post is checked before resolving post config in includes/class-gutenberg-sidebar.php:3811-3839. |
handle_summarize_context() post-id authorization |
Fixed | The route validates edit_post before provider/cost work in includes/class-gutenberg-sidebar.php:6266-6278. |
handle_detect_intent() post-id authorization |
Fixed | The route validates edit_post before provider/cost work in includes/class-gutenberg-sidebar.php:6374-6388. |
handle_refine_multi_pass() post-id authorization |
Fixed | The route validates edit_post before provider/cost work in includes/class-gutenberg-sidebar.php:6756-6770. |
| Migrate-on-read session recovery | Improved, still has one response-identity edge case | get_context() uses the migrated id for lookup, but returns the original $session_id in the response at includes/class-context-service.php:72-90. |
| Provider metadata response contract | Still open | Chat responses include provider transparency; many non-chat responses still omit it. |
Legacy /chat-history endpoint |
Still open | The deprecated route remains registered and still returns legacy post meta in includes/class-gutenberg-sidebar.php:346-354 and includes/class-gutenberg-sidebar.php:1282-1308. |
| Deprecated cost wrapper | Still open as low-risk debt | record_usage() is deprecated but still hard-codes provider openrouter in includes/class-cost-tracker.php:176-186. |
| Model registry/default unification | Still open | Defaults remain spread across activation, settings, providers, JS presets, wrappers, and image manager. |
| Sidebar browser compatibility | Unverified | Syntax passes, but no WordPress editor/browser pass was run. |
Remaining Findings
P1: Non-Chat AI Responses Still Do Not Expose Provider Transparency
Chat now satisfies the provider transparency contract by returning provider, selected_provider, fallback_used, and warnings in normal and streaming completion paths at includes/class-gutenberg-sidebar.php:1049-1053 and includes/class-gutenberg-sidebar.php:1220-1227.
The same contract is still missing from many non-chat AI endpoints:
- Plan generation tracks the actual provider but returns only
plan,cost, andweb_search_resultsatincludes/class-gutenberg-sidebar.php:1992-2013. - Plan revision tracks the actual provider but returns only
planandcostatincludes/class-gutenberg-sidebar.php:2153-2172. - Block refinement returns
blocks,blockId, andcostonly atincludes/class-gutenberg-sidebar.php:4338-4355. - Multi-pass refinement returns
pass,refined_content, andcostonly atincludes/class-gutenberg-sidebar.php:6808-6825.
Impact:
- Users and support logs can see provider/fallback behavior for chat, but not for planning, writing, refinement, clarity, SEO/meta, or utility AI actions.
- When fallback routing or local/cloud routing differs by task, the UI cannot explain why costs, quality, or latency changed.
- Cost records may be accurate internally while the user-visible response stays opaque.
Recommended fix:
- Add one shared response metadata helper, for example
build_provider_metadata( $provider_result ). - Add the same keys to every non-streaming AI response:
providerselected_providerfallback_usedwarnings
- Add equivalent completion metadata to every streaming AI action, not only chat.
- Add focused REST response tests for plan, revise, block refine, refine multi-pass, clarity, and meta generation.
P1: Migrated Context Can Return Messages From One Session But Report Another Session ID
get_context() now does the important migration recovery step: when the requested session does not exist and legacy post history exists, it migrates the legacy history and then fetches the effective migrated session id.
The remaining problem is the response identity:
- Migration returns
$migrated_session_idatincludes/class-context-service.php:72. $effective_session_idis used to fetch$sessionatincludes/class-context-service.php:74-75.- The returned context still uses the original
$session_idatincludes/class-context-service.php:89-90.
Impact:
- A client can receive migrated messages/context but keep using an empty or stale session id.
- That can fragment follow-up chat into a different session, making "conversation memory disappeared" bugs look random.
- This is especially risky during migration from legacy
_wpaw_chat_historyto conversation sessions because it happens only for older posts.
Recommended fix:
- After successful migration lookup, set
$session_id = $effective_session_idbefore returning context. - Prefer returning
$session['session_id'] ?? $effective_session_idif the session object carries its canonical id. - Add a migration test where
get_context( 'missing-session', $post_id )creates a new session and verifies that the responsesession_idequals the migrated session id.
P2: Deprecated /chat-history Still Exposes Legacy Post-Meta History
The legacy route is still registered:
GET /wp-agentic-writer/v1/chat-history/(?P<post_id>\d+)remains active atincludes/class-gutenberg-sidebar.php:346-354.handle_get_chat_history()still returns values from_wpaw_chat_historywith adeprecatedmarker atincludes/class-gutenberg-sidebar.php:1282-1308.get_post_chat_history()still reads_wpaw_chat_historydirectly atincludes/class-gutenberg-sidebar.php:1336-1346.
Impact:
- This is not an immediate permission issue because the route checks
edit_post. - It is still product debt because there are now two observable history APIs: legacy post meta and conversation sessions.
- UI or integrations can keep depending on the old endpoint and bypass the new context/session model.
Recommended fix:
- Replace the response body with a conversation-session-backed compatibility payload, or return a structured
410/deprecation response after one release window. - Track any client code still calling
/chat-historybefore removal. - Add a regression test proving no active UI path uses this endpoint.
P2: Deprecated Cost Wrapper Still Defaults Provider To OpenRouter
record_usage() is now explicitly deprecated, which is good. It still calls record_usage_full() with provider openrouter at includes/class-cost-tracker.php:176-186.
Impact:
- Any remaining caller using the legacy wrapper can create misleading provider attribution.
- This is lower risk than before because newer paths use provider metadata, but it can still pollute reporting.
Recommended fix:
- Change the default provider to
legacy_unknownorunknown. - Log a debug warning when this wrapper is called so remaining callers can be eliminated.
- Search for all direct calls to
record_usage()and migrate them torecord_usage_full().
P2: Model Defaults Are Still Fragmented Across Runtime Surfaces
Model defaults are still declared in multiple places with conflicting generations and ids:
- Activation defaults in
wp-agentic-writer.php:140-142. - Sidebar defaults in
includes/class-gutenberg-sidebar.php:276-283. - Settings defaults and sanitization fallbacks in
includes/class-settings.phpandincludes/class-settings-v2.php. - Provider property defaults in
includes/class-openrouter-provider.php:34-69. - JavaScript presets in
assets/js/settings-v2.js:35-56. - Wrapper fallback groups in
includes/class-wp-ai-client-wrapper.php:94-100. - Image-manager fallbacks in
includes/class-image-manager.php:185-249.
Impact:
- Changing a default model in one layer can silently diverge from another layer.
- UI presets, provider runtime defaults, activation defaults, and cost estimation can disagree.
- This is exactly the kind of area where fixing A can regress B.
Recommended fix:
- Create one PHP model registry for task defaults, labels, capabilities, pricing hints, provider support, and deprecation status.
- Generate or localize the JS presets from that registry instead of hard-coding them independently.
- Add a consistency test that checks all task defaults exist in the registry and that activation/settings/provider fallbacks match it.
P2: Sidebar UI Needs A Real WordPress Editor Compatibility Pass
assets/js/sidebar.js and assets/js/settings-v2.js both pass syntax checks, but syntax is not enough for the editor UI.
Impact:
- WordPress package availability,
wp.editPost.PluginSidebarcompatibility, REST nonce behavior, streaming UI states, and cost display states can still fail only inside the block editor. - The plugin has many user-facing states now: chat, planning, refinement, clarity, model routing, cost display, warnings, and session history. A static pass cannot validate their interaction quality.
Recommended fix:
- Run a browser pass inside the WordPress editor with the plugin enabled.
- Cover at minimum:
- Sidebar opens and persists.
- Chat session continues after reload.
- Provider/fallback warnings render where metadata exists.
- Cost display updates after chat, plan, refine, and meta-generation actions.
- Unauthorized post access fails cleanly without partial UI mutation.
- Model settings changes are reflected in generated requests.
System-Level Opportunities
1. Add A Shared Guard For Post-Scoped REST Requests
The fifth-pass fixes were successful, but they were route-by-route. To keep this from regressing, add a helper such as:
private function require_post_permission_from_request( WP_REST_Request $request, $field = 'postId' ) {
$post_id = absint( $request->get_param( $field ) );
if ( $post_id > 0 && ! $this->check_post_permission( $post_id ) ) {
return new WP_Error( 'forbidden', __( 'You do not have permission to access this post.', 'wp-agentic-writer' ), array( 'status' => 403 ) );
}
return $post_id;
}
Then use it immediately after request parsing in every route that reads, writes, streams, or attributes cost to a post.
2. Standardize AI Response Envelopes
The plugin needs one response shape for all AI actions:
{
"data": {},
"cost": 0,
"provider": "openrouter",
"selected_provider": "openrouter",
"fallback_used": false,
"warnings": []
}
The exact shape can differ, but the metadata keys should not. This will make UI, debugging, and cost review dramatically simpler.
3. Treat Conversation Session ID As A Canonical Contract
The remaining migration edge case is a sign that session_id should be treated as a canonical response contract. Any endpoint that creates, migrates, clears, or resumes a session should return the active session id and the UI should update local state from that value.
Recommended Next Work
- Fix
get_context()so migrated reads return the effective session id. - Add provider metadata to every non-chat AI response and stream completion event.
- Replace or retire
/chat-historycompatibility behavior. - Change deprecated cost wrapper provider attribution from
openroutertolegacy_unknown. - Start model registry consolidation.
- Run the WordPress editor browser pass before calling the audit chain fully closed.
Current Verdict
The fifth-pass implementation is proper for the critical authorization and cost-attribution issues it targeted. I would mark the fifth-pass P0 items complete.
The plugin is not fully audit-clean yet. The next highest-value work is now chat/context correctness plus response transparency: fix the migrated session_id edge case and make provider/cost metadata consistent across every AI action.