Skip to content

fix: guard MemoryFileRegistry against missing agents-api at bootstrap#2006

Open
chubes4 wants to merge 1 commit into
mainfrom
fix-memory-registry-class-guard
Open

fix: guard MemoryFileRegistry against missing agents-api at bootstrap#2006
chubes4 wants to merge 1 commit into
mainfrom
fix-memory-registry-class-guard

Conversation

@chubes4
Copy link
Copy Markdown
Member

@chubes4 chubes4 commented May 14, 2026

Fixes #2005

Summary

  • MemoryFileRegistry fataled during muplugins_loaded whenever DM core booted in an environment where the agents-api substrate wasn't loaded (notably the Homeboy playground used by homeboy test and homeboy release).
  • Five substrate touch-points were unguarded: WP_Agent_Memory_Layer::normalize(), WP_Agent_Context_Injection_Policy::{NEVER,ALWAYS,normalize,is_always_injected}, WP_Agent_Memory_Registry::{register,unregister,reset,get_all}, and \AgentsAPI\AI\Context\WP_Agent_Context_Authority_Tier::* constant access.
  • All five are now wrapped behind a cached agents_api_loaded() helper. Production behavior (substrate loaded) is unchanged.
  • When the substrate is missing, the registry runs degraded against its local self::$files map with canonical string-literal fallbacks (workspace_shared, user_global, agent_identity, agent_memory, always, never) that match the substrate's vocabulary.
  • This unblocks homeboy test and homeboy release for every DM-aware plugin: data-machine-events, data-machine-socials, data-machine-frontend-chat, data-machine-code.

The trace (from #2005)

STAGE_FAIL:load_deps:Error: Class "WP_Agent_Memory_Layer" not found
  at /wordpress/wp-content/plugins/data-machine/inc/Engine/AI/MemoryFileRegistry.php:593
TRACE:
  #0 inc/Engine/AI/MemoryFileRegistry.php(110): MemoryFileRegistry::normalize_layer('shared')
  #1 inc/bootstrap.php(137):                       MemoryFileRegistry::register('SITE.md', 10, Array)
  #2 data-machine.php(35):                         require_once('inc/bootstrap.php')
  #3 homeboy-extension/scripts/lib/playground-bootstrap.php(506)
  #4 runner.php(119): pg_run_load_deps_stage(Array)
  ...
  #8 wp-settings.php(511): do_action('muplugins_loaded')

data-machine-events v0.33.0 had to ship with --skip-checks today because of this.

Before / after

normalize_layer()

Before:

private static function normalize_layer( string $layer ): string {
    return WP_Agent_Memory_Layer::normalize( $layer, self::LAYER_AGENT );
}

After:

private static function normalize_layer( string $layer ): string {
    if ( ! self::agents_api_loaded() ) {
        $valid = array( self::LAYER_SHARED, self::LAYER_AGENT, self::LAYER_USER, self::LAYER_NETWORK );
        return in_array( $layer, $valid, true ) ? $layer : self::LAYER_AGENT;
    }
    return WP_Agent_Memory_Layer::normalize( $layer, self::LAYER_AGENT );
}

default_authority_tier()

Before:

private static function default_authority_tier( string $layer, string $filename ): string {
    if ( self::LAYER_SHARED === $layer || self::LAYER_NETWORK === $layer ) {
        return \AgentsAPI\AI\Context\WP_Agent_Context_Authority_Tier::WORKSPACE_SHARED;
    }
    if ( self::LAYER_USER === $layer ) {
        return \AgentsAPI\AI\Context\WP_Agent_Context_Authority_Tier::USER_GLOBAL;
    }
    if ( 'SOUL.md' === $filename ) {
        return \AgentsAPI\AI\Context\WP_Agent_Context_Authority_Tier::AGENT_IDENTITY;
    }
    return \AgentsAPI\AI\Context\WP_Agent_Context_Authority_Tier::AGENT_MEMORY;
}

After:

private static function default_authority_tier( string $layer, string $filename ): string {
    if ( ! self::agents_api_loaded() ) {
        if ( self::LAYER_SHARED === $layer || self::LAYER_NETWORK === $layer ) {
            return 'workspace_shared';
        }
        if ( self::LAYER_USER === $layer ) {
            return 'user_global';
        }
        if ( 'SOUL.md' === $filename ) {
            return 'agent_identity';
        }
        return 'agent_memory';
    }
    // ... substrate-loaded branch unchanged ...
}

The fallback string literals match WP_Agent_Context_Authority_Tier::ordered() exactly, so downstream string-comparisons keep working in degraded mode.

Why the fix is wider than the issue suggested

The issue body named normalize_layer() (line 593) and default_authority_tier() (line 598) as the two touch-points. Guarding only those would have shifted the fatal one frame deeper: register() itself directly accesses WP_Agent_Context_Injection_Policy::NEVER/ALWAYS constants (then-line 129-130), ::normalize() (then-line 146), and WP_Agent_Memory_Registry::register() (then-line 153) on the same bootstrap path. All four are missing in the playground at the same moment.

So the fix wraps every substrate access reachable during register() / deregister() / reset() / get_resolved() / get_for_mode() behind the same cached guard. Code paths reachable only after agents-api is guaranteed loaded (admin UI handlers, etc.) are deliberately left alone — adding guards there would be dead defensive code.

Test plan

tests/memory-file-registry-missing-agents-api-smoke.php (new — pure PHP, no autoloader pollution)

Deliberately does NOT load vendor/automattic/agents-api/, then exercises every touch-point. 21 assertions, all pass:

  • Precondition: class_exists() returns false for all four substrate classes.
  • register() does not throw.
  • Valid layers (shared, agent, user, network) round-trip through normalize_layer().
  • Unknown layers normalize to LAYER_AGENT.
  • default_authority_tier() returns the canonical string literals (workspace_shared, user_global, agent_identity, agent_memory).
  • Default retrieval_policy is 'always' when modes are declared, 'never' when omitted.
  • get_for_mode('chat') filters correctly without fataling.
  • deregister() and reset() complete without fataling.

Run with: php tests/memory-file-registry-missing-agents-api-smoke.php

tests/Unit/Engine/AI/MemoryFileRegistryTest.php (new — regression for substrate-loaded path)

Six test cases verifying the register() refactor did not change production behavior:

  • test_register_round_trips_through_get_all
  • test_unknown_layer_normalizes_to_agent_default
  • test_no_modes_defaults_to_never_retrieval_policy
  • test_default_authority_tier_uses_canonical_vocabulary
  • test_deregister_removes_entry
  • test_get_for_mode_filters_by_mode_and_policy

homeboy test data-machine (the smoke that motivated this issue)

Run from the worktree:

Tests: 1262, Assertions: 4038, Skipped: 3.
[test-results] Total: 1262, Passed: 1259, Failed: 0, Skipped: 3
Playground test run complete.

Before this PR: STAGE_FAIL:load_deps:Error: Class "WP_Agent_Memory_Layer" not found at the first MemoryFileRegistry::register() call in inc/bootstrap.php. After: full suite runs.

Lint / syntax

  • php -l clean on all three touched files.
  • vendor/bin/phpcs clean on all three touched files.
  • homeboy audit --path .: no new outliers attributable to this change.

Out of scope (per issue body)

  • Decoupling DM core from agents-api entirely. The dependency is correct; we just made it optional during bootstrap/test.
  • Fixing the Homeboy playground bootstrap to honor Requires Plugins. Filed separately upstream.

DM core declares agents-api as a Requires Plugins dependency, which the
WordPress admin UI honors. The Homeboy playground bootstrap (used by
homeboy test and homeboy release on every DM-aware plugin) loads
plugins via require_once and bypasses that gate. That means DM core
can boot in an environment where the agents-api substrate classes do
not exist — and MemoryFileRegistry fataled during muplugins_loaded
because it referenced WP_Agent_Memory_Layer, WP_Agent_Memory_Registry,
WP_Agent_Context_Injection_Policy, and WP_Agent_Context_Authority_Tier
unguarded.

This blocked homeboy test and homeboy release across the DM family
(data-machine-events, data-machine-socials, data-machine-frontend-chat,
data-machine-code, …). data-machine-events v0.33.0 had to ship with
--skip-checks today because of it.

Guard every substrate access in MemoryFileRegistry with a cached
class_exists() check. When the substrate is loaded (production), behavior
is unchanged. When it isn't (playground), the registry runs degraded
against its local self::$files map with canonical string-literal
fallbacks for layer normalization, retrieval policy, and authority tier.

Verified: homeboy test data-machine completes with 1259 passed, 0 failed,
3 skipped — the muplugins_loaded fatal is gone.

Fixes #2005
@homeboy-ci
Copy link
Copy Markdown
Contributor

homeboy-ci Bot commented May 14, 2026

Homeboy Results — data-machine

Lint

lint — passed

ℹ️ Full options: homeboy docs commands/lint
Deep dive: homeboy lint data-machine --changed-since 3de1bc8

Test

test — passed

  • 36 passed

ℹ️ Auto-fix lint issues: homeboy refactor data-machine --from lint --write
ℹ️ Collect coverage: homeboy test data-machine --coverage
ℹ️ Save test baseline: homeboy test data-machine --baseline
ℹ️ Pass args to test runner: homeboy test -- [args]
ℹ️ Full options: homeboy docs commands/test
Deep dive: homeboy test data-machine --changed-since 3de1bc8

Audit

audit — passed

  • test_coverage — 5 finding(s)
  • requested_detectors — 2 finding(s)
  • Total: 7 finding(s)

Deep dive: homeboy audit data-machine --changed-since 3de1bc8

Tooling versions
  • Homeboy CLI: homeboy 0.180.1+f940a180
  • Extension: wordpress from https://github.com/Extra-Chill/homeboy-extensions
  • Extension revision: b753970
  • Action: unknown@unknown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bootstrap fatal: WP_Agent_Memory_Layer not found in MemoryFileRegistry blocks homeboy releases of DM-aware plugins

1 participant