# 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.js` now passes JavaScript syntax validation. - `assets/js/settings-v2.js` still 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: 1. The sidebar debug logger is syntactically valid but recursively calls itself, so any error log path can crash into a stack overflow. 2. Core generation/chat REST endpoints still accept `postId` under generic `edit_posts` permission and then read or write target post meta. 3. 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. 4. 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. 5. 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:17` defines `log` as `wpawLog.log('[WPAW]', ...args)`. - `assets/js/sidebar.js:18` defines `error` as `wpawLog.error('[WPAW]', ...args)`. - `assets/js/sidebar.js:20` defines `warn` as `wpawLog.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()` or `wpawLog.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: - `/chat` is registered with only `check_permissions` at `includes/class-gutenberg-sidebar.php:325-333`. - `/generate-plan` is registered with only `check_permissions` at `includes/class-gutenberg-sidebar.php:377-385`. - `/execute-article` is registered with only `check_permissions` at `includes/class-gutenberg-sidebar.php:398-406`. - `check_permissions()` only checks `current_user_can( 'edit_posts' )` at `includes/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 at `includes/class-gutenberg-sidebar.php:955-978`. - `handle_generate_plan()` reads post memory and writes `_wpaw_plan`, `_wpaw_detected_language`, and `_wpaw_memory` at `includes/class-gutenberg-sidebar.php:1825-1969`. - `handle_execute_article()` reads `_wpaw_plan` and can update `_wpaw_plan` after generation at `includes/class-gutenberg-sidebar.php:2925-3176`. - `handle_reformat_blocks()` reads `_wpaw_plan` at `includes/class-gutenberg-sidebar.php:3509-3529`. - `handle_regenerate_block()` accepts `postId` and records cost against it at `includes/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 `postId` or reads/writes post meta, not only the endpoints listed in a prior audit. - For postless conversation mode, allow `postId = 0` only 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()` reads `sessionId` at `includes/class-gutenberg-sidebar.php:1230-1234`. - It calls `$this->context_service->clear_context( $session_id, $post_id )` at `includes/class-gutenberg-sidebar.php:1243-1244`. But both frontend clear calls still send only the post id: - Reset command sends `JSON.stringify({ postId: postId })` at `assets/js/sidebar.js:1661-1669`. - Clear chat context sends `JSON.stringify({ postId })` at `assets/js/sidebar.js:2023-2031`. - A search of `assets/js/sidebar.js` found no active `sessionId`/`currentSession` state 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 `sessionId` on `/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\d+)` is still registered at `includes/class-gutenberg-sidebar.php:346-354`. - `handle_get_chat_history()` reads legacy post meta at `includes/class-gutenberg-sidebar.php:1261-1277`. - `update_post_chat_history()` still writes `_wpaw_chat_history` at `includes/class-gutenberg-sidebar.php:1289-1322`. - `migrate_legacy_chat_history()` still keeps `_wpaw_chat_history` after migration because deletion is commented out at `includes/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 at `includes/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-history` endpoint once the frontend uses conversation sessions. - Delete legacy post meta after successful migration, or write a `_wpaw_chat_history_migrated` marker 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 = '' )` at `includes/class-cost-tracker.php:164`. - It always records provider as `'openrouter'` at `includes/class-cost-tracker.php:172`. - The WP AI core path passes `'core'` as the `$model` argument at `includes/class-wp-ai-client-wrapper.php:197-202`. - The legacy provider path passes `$provider_result->actual_provider` as the `$model` argument at `includes/class-wp-ai-client-wrapper.php:249-254`. Impact: - Cost rows from WP AI wrapper paths can show model=`core` or model=`local_backend/openrouter` and 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()` calls `DESCRIBE {$table_name}` at `includes/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_version` is current at `wp-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 LIKE` guard before `DESCRIBE`. - 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`, and `cost` where 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.php` and `includes/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`, and `includes/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_model` only 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_history` routes 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.