15 KiB
WP Agentic Writer Third Retrace Audit
Status: COMPLETE / SUPERSEDED
Completion marker date: 2026-05-25
Next retrace report: docs/architecture/PLUGIN_AUDIT_RETRACE_FOURTH_PASS_2026-05-25.md
Audit date: 2026-05-24
Baseline retraced: docs/architecture/PLUGIN_AUDIT_RETRACE_SECOND_PASS_2026-05-24.md
Scope: third pass after second-retrace implementation, covering UI/UX, editor runtime, REST authorization, conversation context/history, cost tracking, provider/model metadata, migrations, and release readiness.
Executive Summary
The second-pass implementation closed several concrete blockers:
assets/js/sidebar.jsnow passes JavaScript syntax validation.assets/js/settings-v2.jsstill passes JavaScript syntax validation.- Full PHP syntax validation passes.
WP_Agentic_Writer_Cost_Tracker::record_usage()now exists, so the previous missing-method fatal is closed.- Conversation migration now reads
wpaw_conversations_db_version. - Deactivation now clears
wpaw_cleanup_old_sessions. - Previously named post-config, cost, section-block, and image REST handlers now check target-post permissions.
The plugin is still not ready to move exclusively into new chat/context feature work. The largest remaining risks are now more subtle:
- The sidebar debug logger is syntactically valid but recursively calls itself, so any error log path can crash into a stack overflow.
- Core generation/chat REST endpoints still accept
postIdunder genericedit_postspermission and then read or write target post meta. - Clear context accepts
sessionId, but the current frontend calls do not send one, and the sidebar no longer appears to maintain a session id in state. - The legacy chat-history route and legacy post-meta helper methods remain active, while the context service comment says legacy history should migrate on read.
- Cost compatibility tracking no longer fatals, but it records misleading provider/model metadata.
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 trace of second-pass findings against current code.
- No live WordPress browser workflow was run in this pass.
Second-Pass Status Trace
| Second-pass item | Current status | Evidence |
|---|---|---|
| Sidebar JS parse failure | Fixed, but runtime logger bug remains | node -c assets/js/sidebar.js passes; logger recursively calls itself at assets/js/sidebar.js:17-20. |
Missing record_usage() method |
Fixed, but attribution is inaccurate | Method exists at includes/class-cost-tracker.php:164-176; wrapper still passes provider names through the $model parameter. |
| Conversation migration version gate | Improved | wpaw_run_migrations() reads wpaw_conversations_db_version at includes/class-conversation-migration.php:67-72; main table bootstrap also checks it at wp-agentic-writer.php:250-259. |
| Clear-context uses context service | Partially fixed | Handler accepts sessionId and calls context service at includes/class-gutenberg-sidebar.php:1230-1244; frontend clear calls omit sessionId. |
| Post-config/cost/section/image permissions | Fixed for named handlers | Target post checks added in the handlers traced below. |
| Cost table base schema | Improved | Base schema includes session_id, provider, and status; runtime upgrade still assumes the table exists. |
| Deactivation cleanup | Fixed | wp_clear_scheduled_hook( 'wpaw_cleanup_old_sessions' ) added at wp-agentic-writer.php:271-278. |
| Model registry/default unification | Still open | Defaults remain spread across activation, settings PHP, settings JS, providers, image manager, and WP AI wrapper. |
Critical Findings
P0: Sidebar Logger Recurses Instead of Calling Console
The syntax error from the last report is fixed, but the logger implementation is still broken:
assets/js/sidebar.js:17defineslogaswpawLog.log('[WPAW]', ...args).assets/js/sidebar.js:18defineserroraswpawLog.error('[WPAW]', ...args).assets/js/sidebar.js:20defineswarnaswpawLog.warn('[WPAW]', ...args).
Impact:
- Any
wpawLog.error()call can recurse until the browser throws a maximum call stack error. - With debug enabled, any
wpawLog.log()orwpawLog.warn()call can do the same. - Error handling becomes unreliable exactly when the sidebar needs it most, especially during streaming, generation, migration, and cost refresh failures.
Recommended fix:
- Change the sidebar logger to match the working settings logger:
log: (...args) => { if (isDebug) console.log('[WPAW]', ...args); }error: (...args) => console.error('[WPAW]', ...args)info: (...args) => { if (isDebug) console.info('[WPAW]', ...args); }warn: (...args) => { if (isDebug) console.warn('[WPAW]', ...args); }
- Add a simple static check or unit test that rejects
wpawLog.*inside the logger object itself.
P0: Core Post-Scoped Generation Routes Still Lack Upfront edit_post Checks
The named endpoints from the previous report were patched, but the larger route family still relies on generic edit_posts route permission:
/chatis registered with onlycheck_permissionsatincludes/class-gutenberg-sidebar.php:325-333./generate-planis registered with onlycheck_permissionsatincludes/class-gutenberg-sidebar.php:377-385./execute-articleis registered with onlycheck_permissionsatincludes/class-gutenberg-sidebar.php:398-406.check_permissions()only checkscurrent_user_can( 'edit_posts' )atincludes/class-gutenberg-sidebar.php:930-932.
Handlers then use postId to read or mutate post-scoped data:
handle_chat_request()reads post config, detected language, and focus keyword from the target post atincludes/class-gutenberg-sidebar.php:955-978.handle_generate_plan()reads post memory and writes_wpaw_plan,_wpaw_detected_language, and_wpaw_memoryatincludes/class-gutenberg-sidebar.php:1825-1969.handle_execute_article()reads_wpaw_planand can update_wpaw_planafter generation atincludes/class-gutenberg-sidebar.php:2925-3176.handle_reformat_blocks()reads_wpaw_planatincludes/class-gutenberg-sidebar.php:3509-3529.handle_regenerate_block()acceptspostIdand records cost against it atincludes/class-gutenberg-sidebar.php:3594-3642.
Impact:
- A user with generic author/editor access can potentially read or affect plugin state for posts they cannot edit.
- Cost records can be attributed to unauthorized posts.
- Chat/context behavior can leak focus keywords, detected language, memory summaries, and plans across post boundaries.
Recommended fix:
- Add an upfront helper such as
require_post_permission_from_params( $params, 'postId' ). - Call it in every handler that accepts
postIdor reads/writes post meta, not only the endpoints listed in a prior audit. - For postless conversation mode, allow
postId = 0only when no post meta is read or written.
High Priority Findings
P1: Clear Context Still Does Not Clear the Active Session From the UI
The backend handler now accepts sessionId and calls the context service:
handle_clear_context()readssessionIdatincludes/class-gutenberg-sidebar.php:1230-1234.- It calls
$this->context_service->clear_context( $session_id, $post_id )atincludes/class-gutenberg-sidebar.php:1243-1244.
But both frontend clear calls still send only the post id:
- Reset command sends
JSON.stringify({ postId: postId })atassets/js/sidebar.js:1661-1669. - Clear chat context sends
JSON.stringify({ postId })atassets/js/sidebar.js:2023-2031. - A search of
assets/js/sidebar.jsfound no activesessionId/currentSessionstate being maintained.
Impact:
- Clear context deletes legacy post meta but does not clear the active conversation table row when the session id is omitted.
- Users can see a cleared frontend, then later reload or resume a session that still contains old messages.
- This undermines the goal of making the conversations table the source of truth.
Recommended fix:
- Reintroduce explicit active session state in the sidebar.
- Send
sessionIdon/clear-context. - If the UI cannot provide a session id, the backend should clear active sessions for that post owned by the current user or return a clear error explaining that the session id is required.
P1: Legacy Chat History Read/Write Helpers Still Exist Beside the Session Store
The main chat send path no longer writes _wpaw_chat_history, but old route/helper code remains:
/chat-history/(?P<post_id>\d+)is still registered atincludes/class-gutenberg-sidebar.php:346-354.handle_get_chat_history()reads legacy post meta atincludes/class-gutenberg-sidebar.php:1261-1277.update_post_chat_history()still writes_wpaw_chat_historyatincludes/class-gutenberg-sidebar.php:1289-1322.migrate_legacy_chat_history()still keeps_wpaw_chat_historyafter migration because deletion is commented out atincludes/class-context-service.php:299-300.- The context service header says legacy history migrates on read, but
get_context()does not call migration when no session exists atincludes/class-context-service.php:62-87.
Impact:
- The system still has two history stores and one migration route, rather than one reliable source of truth.
- Legacy meta can be migrated repeatedly or surfaced by old endpoints after the active session has diverged.
- Future chat/context work remains likely to regress because old and new paths coexist.
Recommended fix:
- Remove or deprecate the legacy
/chat-historyendpoint once the frontend uses conversation sessions. - Delete legacy post meta after successful migration, or write a
_wpaw_chat_history_migratedmarker and never re-import it. - Make
get_context()perform migrate-on-read if the post has legacy history and no session exists.
P1: Cost Compatibility Method Prevents Fatal, But Mislabels Model/Provider
record_usage() now exists, but its signature and callers do not align with the newer cost schema:
- Method signature is
record_usage( $post_id, $action, $model, $cost, $session_id = '' )atincludes/class-cost-tracker.php:164. - It always records provider as
'openrouter'atincludes/class-cost-tracker.php:172. - The WP AI core path passes
'core'as the$modelargument atincludes/class-wp-ai-client-wrapper.php:197-202. - The legacy provider path passes
$provider_result->actual_provideras the$modelargument atincludes/class-wp-ai-client-wrapper.php:249-254.
Impact:
- Cost rows from WP AI wrapper paths can show model=
coreor model=local_backend/openrouterand provider=openrouter, regardless of the actual provider/model. - Cost analytics, provider comparison, and budget debugging become unreliable.
Recommended fix:
- Change
record_usage()to accept an options array or explicit$provider,$model,$input_tokens,$output_tokens,$session_id,$status. - Update wrapper callers to pass the selected model and actual provider separately.
- Keep the old positional method only as a deprecated compatibility wrapper.
P1: Cost Table Runtime Upgrade Still Does Not Self-Heal a Missing Table
The base schema is improved, but runtime upgrade remains fragile:
maybe_upgrade_table()callsDESCRIBE {$table_name}atincludes/class-cost-tracker.php:71-76.- It does not check whether the table exists before calling
in_array()on the column list. - Main table creation can still be skipped when
wpaw_db_versionis current atwp-agentic-writer.php:230-260.
Impact:
- A site with a current main DB version but missing cost table may not recover cleanly.
- First access to the cost tracker can warn or fail before any cost row is recorded.
Recommended fix:
- Add a
SHOW TABLES LIKEguard beforeDESCRIBE. - If the table is missing, call the idempotent cost table creator.
- Prefer per-table schema checks over relying only on
wpaw_db_version.
Medium Priority Findings
P2: Provider Metadata Is Still Incomplete Outside Chat
Chat responses and streaming completion events include provider metadata, but several non-chat routes still return only generated content, variants, or blocks. Cost records often receive provider metadata through the hook, but the UI/API response contract is not uniform.
Recommended fix:
- Standardize generated response envelopes across plan, execute, refine, keyword, image recommendation, and image variant endpoints.
- Include
provider,selected_provider,fallback_used,warnings,model, andcostwhere available.
P2: Model Defaults Remain Fragmented
Defaults still exist in multiple places:
- Activation defaults in
wp-agentic-writer.php. - Sidebar localized defaults in
includes/class-gutenberg-sidebar.php. - Settings V1/V2 PHP defaults in
includes/class-settings.phpandincludes/class-settings-v2.php. - JS presets in
assets/js/settings-v2.js. - Provider defaults in
includes/class-openrouter-provider.php,includes/class-local-backend-provider.php,includes/class-codex-provider.php, andincludes/class-wp-ai-client-wrapper.php. - Image defaults in
includes/class-image-manager.php.
Impact:
- A model update can appear fixed in settings but remain stale in provider/runtime paths.
- Cost estimates, UI presets, and actual generation model can diverge.
Recommended fix:
- Create a PHP model registry/default resolver and localize its resolved values to JS.
- Keep legacy keys such as
execution_modelonly as migrations into canonical task keys.
P2: Sidebar Uses wp.editPost Without the Previous Fallback
The current sidebar imports PluginSidebar from wp.editPost at assets/js/sidebar.js:8-10. The previous code had a fallback for wp.editor versus wp.editPost. This may be intentional, but it should be verified against the WordPress versions the plugin supports.
Recommended fix:
- Browser-test the sidebar on the minimum supported WordPress version.
- Restore a compatibility fallback if needed.
Definition of Done Gates for This Pass
Before considering this retrace implementation complete:
- Fix sidebar logger recursion and verify error/debug calls in browser devtools.
- Add target-post permission checks to all handlers that accept
postId, especially chat, generate-plan, execute-article, reformat-blocks, regenerate-block, clarity/SEO/GEO, refinement, and any post-scoped streaming path. - Make clear-context clear the actual active session from the UI, not only legacy post meta.
- Remove, migrate, or hard-deprecate legacy
_wpaw_chat_historyroutes and helper writes. - Fix
record_usage()metadata attribution so model and provider are not swapped or hardcoded. - Add a missing-table guard to the cost tracker migration path.
- Keep PHP and JS syntax checks passing.
Current Decision
Do not move fully into new chat/context implementation yet. The second-pass checklist is mostly implemented, but the current code still has one sidebar runtime blocker, one broad post-authorization gap, and an incomplete conversation source-of-truth transition. Fix those first, then chat/context work will have a much firmer base.