From f6607ded9d89e483f305894a3f77447c31ba0329 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 6 May 2026 11:38:33 -0400 Subject: [PATCH 1/2] fix: exclude non-eager memory files from prompts --- inc/Engine/AI/MemoryFileRegistry.php | 17 ++++++++--- .../AI/Memory/MemoryPolicyResolverTest.php | 28 +++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/inc/Engine/AI/MemoryFileRegistry.php b/inc/Engine/AI/MemoryFileRegistry.php index be531171..9b25f5f1 100644 --- a/inc/Engine/AI/MemoryFileRegistry.php +++ b/inc/Engine/AI/MemoryFileRegistry.php @@ -77,10 +77,13 @@ class MemoryFileRegistry { * A capability string (e.g. 'manage_options') = editable only * by users with that WordPress capability. Default true. * Forced to false when composable is true. - * @type string[] $modes Execution modes where this file should be injected. + * @type string[] $modes Execution modes where this file is available. * Array of mode slugs (e.g. 'chat', 'editor', 'pipeline', - * 'system') or array( 'all' ) to inject everywhere. + * 'system') or array( 'all' ) to make available everywhere. * Default array( 'all' ). + * @type string $retrieval_policy Context injection policy. Only `always` files are injected + * by CoreMemoryFilesDirective. Use `never` for files that exist + * for external/runtime projection only. * @type bool $composable Whether this file is auto-generated from registered sections * via SectionRegistry. Composable files are regenerated on * demand and are not hand-editable. Default false. @@ -382,10 +385,11 @@ function ( $meta ) use ( $layer ) { } /** - * Get all files applicable to a specific agent mode. + * Get always-injected files applicable to a specific agent mode. * * Returns files that either list the mode in their `modes` array - * or are registered with `['all']` (the default). + * or are registered with `['all']` (the default), excluding files + * whose retrieval policy says they should not be injected eagerly. * * @since 0.60.0 * @since 0.68.0 Internal key renamed from contexts to modes. @@ -403,6 +407,11 @@ public static function get_for_mode( string $mode ): array { return array_filter( self::get_resolved(), function ( $meta ) use ( $mode ) { + $retrieval_policy = $meta['retrieval_policy'] ?? WP_Agent_Context_Injection_Policy::ALWAYS; + if ( ! WP_Agent_Context_Injection_Policy::is_always_injected( $retrieval_policy ) ) { + return false; + } + $modes = $meta['modes'] ?? array( self::MODE_ALL ); return in_array( self::MODE_ALL, $modes, true ) || in_array( $mode, $modes, true ); diff --git a/tests/Unit/AI/Memory/MemoryPolicyResolverTest.php b/tests/Unit/AI/Memory/MemoryPolicyResolverTest.php index 958ce5e8..e241eb33 100644 --- a/tests/Unit/AI/Memory/MemoryPolicyResolverTest.php +++ b/tests/Unit/AI/Memory/MemoryPolicyResolverTest.php @@ -64,6 +64,15 @@ public function set_up(): void { 'modes' => array( 'chat' ), ) ); + MemoryFileRegistry::register( + 'EXTERNAL_RUNTIME.md', + 50, + array( + 'layer' => MemoryFileRegistry::LAYER_SHARED, + 'modes' => array( 'all' ), + 'retrieval_policy' => \WP_Agent_Context_Injection_Policy::NEVER, + ) + ); $this->resolver = new MemoryPolicyResolver(); } @@ -90,6 +99,7 @@ public function test_registered_chat_context_includes_all_and_chat_files(): void $this->assertArrayHasKey( 'MEMORY.md', $files ); $this->assertArrayHasKey( 'USER.md', $files ); $this->assertArrayHasKey( 'CHAT_ONLY.md', $files ); + $this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $files ); } public function test_registered_pipeline_context_excludes_chat_only(): void { @@ -103,6 +113,24 @@ public function test_registered_pipeline_context_excludes_chat_only(): void { $this->assertArrayHasKey( 'MEMORY.md', $files ); $this->assertArrayHasKey( 'USER.md', $files ); $this->assertArrayNotHasKey( 'CHAT_ONLY.md', $files ); + $this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $files ); + } + + public function test_registered_never_retrieval_policy_is_not_injected_in_any_mode(): void { + $chat_files = $this->resolver->resolveRegistered( + array( + 'mode' => MemoryPolicyResolver::MODE_CHAT, + ) + ); + $pipeline_files = $this->resolver->resolveRegistered( + array( + 'mode' => MemoryPolicyResolver::MODE_PIPELINE, + ) + ); + + $this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $chat_files ); + $this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $pipeline_files ); + $this->assertArrayHasKey( 'EXTERNAL_RUNTIME.md', MemoryFileRegistry::get_all() ); } public function test_registered_preserves_metadata(): void { From 88d5969181fa203ded422fef55d311dc2086bf4b Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Wed, 6 May 2026 11:41:33 -0400 Subject: [PATCH 2/2] fix: require explicit modes for memory injection --- inc/Engine/AI/MemoryFileRegistry.php | 39 +++++++++++++------ inc/bootstrap.php | 3 ++ .../AI/Memory/MemoryPolicyResolverTest.php | 7 ++-- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/inc/Engine/AI/MemoryFileRegistry.php b/inc/Engine/AI/MemoryFileRegistry.php index 9b25f5f1..6e96cf04 100644 --- a/inc/Engine/AI/MemoryFileRegistry.php +++ b/inc/Engine/AI/MemoryFileRegistry.php @@ -42,6 +42,14 @@ class MemoryFileRegistry { */ const MODE_ALL = 'all'; + /** + * Default mode list for registered files. + * + * Files without explicit modes are registered and manageable, but are not + * injected into AI prompts. Prompt injection must be an explicit opt-in. + */ + const MODES_NONE = array(); + /** * Registered memory files. * @@ -77,13 +85,13 @@ class MemoryFileRegistry { * A capability string (e.g. 'manage_options') = editable only * by users with that WordPress capability. Default true. * Forced to false when composable is true. - * @type string[] $modes Execution modes where this file is available. + * @type string[] $modes Execution modes where this file is available for prompt injection. * Array of mode slugs (e.g. 'chat', 'editor', 'pipeline', * 'system') or array( 'all' ) to make available everywhere. - * Default array( 'all' ). + * Default empty array: registered but never injected. * @type string $retrieval_policy Context injection policy. Only `always` files are injected - * by CoreMemoryFilesDirective. Use `never` for files that exist - * for external/runtime projection only. + * by CoreMemoryFilesDirective. Defaults to `never` when modes are + * omitted, otherwise `always`. * @type bool $composable Whether this file is auto-generated from registered sections * via SectionRegistry. Composable files are regenerated on * demand and are not hand-editable. Default false. @@ -111,12 +119,15 @@ public static function register( string $filename, int $priority = 50, array $ar $editable = true; } - // Normalize modes: array of slugs, or ['all'] (default). - $modes = $args['modes'] ?? array( self::MODE_ALL ); - if ( ! is_array( $modes ) || empty( $modes ) ) { - $modes = array( self::MODE_ALL ); + // Normalize modes: omitted means registered but not prompt-injected. + $modes = $args['modes'] ?? self::MODES_NONE; + if ( ! is_array( $modes ) ) { + $modes = self::MODES_NONE; } $modes = array_values( array_unique( array_map( 'sanitize_key', $modes ) ) ); + $default_retrieval_policy = empty( $modes ) + ? WP_Agent_Context_Injection_Policy::NEVER + : WP_Agent_Context_Injection_Policy::ALWAYS; // Convention path: relative path from ABSPATH for an additional copy. $convention_path = isset( $args['convention_path'] ) ? ltrim( $args['convention_path'], '/' ) : ''; @@ -132,7 +143,7 @@ public static function register( string $filename, int $priority = 50, array $ar 'modes' => $modes, 'label' => $args['label'] ?? self::filename_to_label( $filename ), 'description' => $args['description'] ?? '', - 'retrieval_policy' => WP_Agent_Context_Injection_Policy::normalize( $args['retrieval_policy'] ?? WP_Agent_Context_Injection_Policy::ALWAYS ), + 'retrieval_policy' => WP_Agent_Context_Injection_Policy::normalize( $args['retrieval_policy'] ?? $default_retrieval_policy ), 'authority_tier' => $args['authority_tier'] ?? self::default_authority_tier( $layer, $filename ), 'provenance' => is_array( $args['provenance'] ?? null ) ? $args['provenance'] : self::default_provenance( $filename ), ); @@ -388,7 +399,7 @@ function ( $meta ) use ( $layer ) { * Get always-injected files applicable to a specific agent mode. * * Returns files that either list the mode in their `modes` array - * or are registered with `['all']` (the default), excluding files + * or are registered with `['all']`, excluding files * whose retrieval policy says they should not be injected eagerly. * * @since 0.60.0 @@ -412,7 +423,11 @@ function ( $meta ) use ( $mode ) { return false; } - $modes = $meta['modes'] ?? array( self::MODE_ALL ); + $modes = $meta['modes'] ?? self::MODES_NONE; + if ( empty( $modes ) ) { + return false; + } + return in_array( self::MODE_ALL, $modes, true ) || in_array( $mode, $modes, true ); } @@ -554,7 +569,7 @@ private static function from_agents_api_sources( array $sources ): array { 'editable' => $source['editable'] ?? true, 'composable' => (bool) ( $source['composable'] ?? false ), 'convention_path' => is_string( $source['convention_path'] ?? null ) ? $source['convention_path'] : '', - 'modes' => is_array( $source['modes'] ?? null ) ? $source['modes'] : array( self::MODE_ALL ), + 'modes' => is_array( $source['modes'] ?? null ) ? $source['modes'] : self::MODES_NONE, 'label' => is_string( $source['label'] ?? null ) ? $source['label'] : self::filename_to_label( $filename ), 'description' => is_string( $source['description'] ?? null ) ? $source['description'] : '', 'retrieval_policy' => is_string( $source['retrieval_policy'] ?? null ) ? $source['retrieval_policy'] : WP_Agent_Context_Injection_Policy::ALWAYS, diff --git a/inc/bootstrap.php b/inc/bootstrap.php index 1b89b019..d3c40f64 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -138,6 +138,7 @@ function () { 'layer' => MemoryFileRegistry::LAYER_SHARED, 'protected' => true, 'composable' => true, + 'modes' => array( MemoryFileRegistry::MODE_ALL ), 'label' => 'Site Context', 'description' => 'Auto-generated site context. Composable — extend via SectionRegistry.', ) ); @@ -145,6 +146,7 @@ function () { 'layer' => MemoryFileRegistry::LAYER_SHARED, 'protected' => true, 'editable' => 'manage_options', + 'modes' => array( MemoryFileRegistry::MODE_ALL ), 'label' => 'Site Rules', 'description' => 'Behavioral constraints that apply to every agent. Admin-editable.', ) ); @@ -186,6 +188,7 @@ function () { 'layer' => MemoryFileRegistry::LAYER_NETWORK, 'protected' => true, 'composable' => true, + 'modes' => array( MemoryFileRegistry::MODE_ALL ), 'label' => 'Network Context', 'description' => 'Auto-generated multisite network topology. Composable — extend via SectionRegistry.', ) ); diff --git a/tests/Unit/AI/Memory/MemoryPolicyResolverTest.php b/tests/Unit/AI/Memory/MemoryPolicyResolverTest.php index e241eb33..7593d044 100644 --- a/tests/Unit/AI/Memory/MemoryPolicyResolverTest.php +++ b/tests/Unit/AI/Memory/MemoryPolicyResolverTest.php @@ -68,9 +68,7 @@ public function set_up(): void { 'EXTERNAL_RUNTIME.md', 50, array( - 'layer' => MemoryFileRegistry::LAYER_SHARED, - 'modes' => array( 'all' ), - 'retrieval_policy' => \WP_Agent_Context_Injection_Policy::NEVER, + 'layer' => MemoryFileRegistry::LAYER_SHARED, ) ); @@ -116,7 +114,7 @@ public function test_registered_pipeline_context_excludes_chat_only(): void { $this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $files ); } - public function test_registered_never_retrieval_policy_is_not_injected_in_any_mode(): void { + public function test_registered_file_without_modes_is_not_injected_in_any_mode(): void { $chat_files = $this->resolver->resolveRegistered( array( 'mode' => MemoryPolicyResolver::MODE_CHAT, @@ -131,6 +129,7 @@ public function test_registered_never_retrieval_policy_is_not_injected_in_any_mo $this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $chat_files ); $this->assertArrayNotHasKey( 'EXTERNAL_RUNTIME.md', $pipeline_files ); $this->assertArrayHasKey( 'EXTERNAL_RUNTIME.md', MemoryFileRegistry::get_all() ); + $this->assertSame( \WP_Agent_Context_Injection_Policy::NEVER, MemoryFileRegistry::get_all()['EXTERNAL_RUNTIME.md']['retrieval_policy'] ); } public function test_registered_preserves_metadata(): void {