Files
wp-agentic-writer/docs/architecture/PLUGIN_AUDIT_RETRACE_THIRD_PASS_2026-05-24.md

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.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<post_id>\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.